From 2dd562b1e11da593e996240f25c3a3ca136cd050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=80=E5=8D=93=E7=96=8C?= <55120045+WowbaggersLiquidLunch@users.noreply.github.com> Date: Fri, 27 May 2022 00:01:05 +0800 Subject: [PATCH 1/9] comply `SymbolGraph.SemanticVersion` to SemVer 2.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a correct but somewhat over-engineered reinplementation of `SymbolGraph.SemanticVersion`. The most important (and also most invasive) changes are the handling of pre-release and build metadata. These include their validation and (their contribution to version-) comparison. Some less important changes include the following: - Version core identifiers are changed to be backed by `UInt`, because they’re not allowed be less than 0. This might appear to be a departure from the convention of using `Int` for even always-positive integers (e.g. `Array.Index`). However, in this specific case, the use of version coure identifiers is mostly contained in the domain of constructing semantic versions, and does not have any apparent need to participate in numeric operations outside of the domain other than comparing individual identifiers. And considering Swift’s current lack of build-time evaluation and error handling, using `UInt` looks like the right trade off between ergnomics and “safety”. - Deprecated the initializer for a new one that throws error, so that erros can be handled instead of runtime trapping. - Added some facilities for inspecting and working with versions’ semantics in general. - Updated the tools version specifier in the main package manifest to “5.6”, because Swift 5.2 has some type-checking bugs that affect the new implementation. - Added a lot of tests, which should cover a large area of (edge) cases. --- Package.swift | 2 +- .../SymbolGraph/SemanticVersion.swift | 69 ---- .../SemanticVersion/Prerelease.swift | 158 ++++++++ .../SemanticVersion+Comparable.swift | 38 ++ ...anticVersion+CustomStringConvertible.swift | 23 ++ .../SemanticVersion/SemanticVersion.swift | 120 ++++++ .../SemanticVersionError.swift | 120 ++++++ .../SematicVerion+Codable.swift | 53 +++ .../LineList/SemanticVersionTests.swift | 26 -- .../SemanticVersion/CodingTests.swift | 94 +++++ .../SemanticVersion/ErrorTests.swift | 376 ++++++++++++++++++ .../MainInitializerTests.swift | 117 ++++++ .../SemanticVersion/PrecedenceTests.swift | 254 ++++++++++++ .../SemanticVersion/SemanticsTests.swift | 137 +++++++ .../StringConversionTests.swift | 103 +++++ .../SymbolGraphCreationTests.swift | 8 +- 16 files changed, 1596 insertions(+), 102 deletions(-) delete mode 100644 Sources/SymbolKit/SymbolGraph/SemanticVersion.swift create mode 100644 Sources/SymbolKit/SymbolGraph/SemanticVersion/Prerelease.swift create mode 100644 Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion+Comparable.swift create mode 100644 Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion+CustomStringConvertible.swift create mode 100644 Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion.swift create mode 100644 Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersionError.swift create mode 100644 Sources/SymbolKit/SymbolGraph/SemanticVersion/SematicVerion+Codable.swift delete mode 100644 Tests/SymbolKitTests/SymbolGraph/LineList/SemanticVersionTests.swift create mode 100644 Tests/SymbolKitTests/SymbolGraph/SemanticVersion/CodingTests.swift create mode 100644 Tests/SymbolKitTests/SymbolGraph/SemanticVersion/ErrorTests.swift create mode 100644 Tests/SymbolKitTests/SymbolGraph/SemanticVersion/MainInitializerTests.swift create mode 100644 Tests/SymbolKitTests/SymbolGraph/SemanticVersion/PrecedenceTests.swift create mode 100644 Tests/SymbolKitTests/SymbolGraph/SemanticVersion/SemanticsTests.swift create mode 100644 Tests/SymbolKitTests/SymbolGraph/SemanticVersion/StringConversionTests.swift diff --git a/Package.swift b/Package.swift index 6d82c3b..ebb0c07 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.2 +// swift-tools-version:5.6 // In order to support users running on the latest Xcodes, please ensure that // Package@swift-5.5.swift is kept in sync with this file. /* diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion.swift deleted file mode 100644 index 537e6de..0000000 --- a/Sources/SymbolKit/SymbolGraph/SemanticVersion.swift +++ /dev/null @@ -1,69 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021 Apple Inc. and the Swift project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See https://swift.org/LICENSE.txt for license information - See https://swift.org/CONTRIBUTORS.txt for Swift project authors -*/ - -extension SymbolGraph { - /// A [semantic version](https://semver.org). - public struct SemanticVersion: Codable, Equatable, CustomStringConvertible { - /** - * The major version number. - * - * For example, the `1` in `1.2.3` - */ - public var major: Int - /** - * The minor version number. - * - * For example, the `2` in `1.2.3` - */ - public var minor: Int - /** - * The patch version number. - * - * For example, the `3` in `1.2.3` - */ - public var patch: Int - - /// The optional prerelease version component, which may contain non-numeric characters. - /// - /// For example, the `4` in `1.2.3-4`. - public var prerelease: String? - - /// Optional build metadata. - public var buildMetadata: String? - - public init(major: Int, minor: Int, patch: Int, prerelease: String? = nil, buildMetadata: String? = nil) { - self.major = major - self.minor = minor - self.patch = patch - self.prerelease = prerelease - self.buildMetadata = buildMetadata - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - major = try container.decode(Int.self, forKey: .major) - minor = try container.decodeIfPresent(Int.self, forKey: .minor) ?? 0 - patch = try container.decodeIfPresent(Int.self, forKey: .patch) ?? 0 - prerelease = try container.decodeIfPresent(String.self, forKey: .prerelease) - buildMetadata = try container.decodeIfPresent(String.self, forKey: .buildMetadata) - } - - public var description: String { - var result = "\(major).\(minor).\(patch)" - if let prerelease = prerelease { - result += "-\(prerelease)" - } - if let buildMetadata = buildMetadata { - result += "+\(buildMetadata)" - } - return result - } - } -} diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/Prerelease.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/Prerelease.swift new file mode 100644 index 0000000..e7208ff --- /dev/null +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/Prerelease.swift @@ -0,0 +1,158 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +extension SymbolGraph.SemanticVersion { + /// A storage for pre-release identifiers. + internal struct Prerelease { + /// The identifiers. + internal let identifiers: [Identifier] + /// A pre-release identifier. + internal enum Identifier { + /// A numeric pre-release identifier. + /// - Parameter identifier: The identifier. + case numeric(_ identifier: UInt) + /// An alphanumeric pre-release identifier. + /// - Parameter identifier: The identifier. + case alphanumeric(_ identifier: String) + } + } +} + +// MARK: - Initializers + +extension SymbolGraph.SemanticVersion.Prerelease { + /// Creates a semantic version pre-release from the given pre-release string. + /// - Note: Empty string translates to an empty pre-release identifier, which is invalid. + /// - Parameter dotSeparatedIdentifiers: The given pre-release string to create a semantic version pre-release from. + /// - Throws: A `SymbolGraph.SemanticVersionError` instance if `dotSeparatedIdentifiers` is not a valid pre-release string. + internal init(_ dotSeparatedIdentifiers: String?) throws { + guard let dotSeparatedIdentifiers = dotSeparatedIdentifiers else { + // FIXME: initialize 'identifiers' directly here after [SR-15670](https://bugs.swift.org/projects/SR/issues/SR-15670?filter=allopenissues) is resolved + // currently 'identifiers' cannot be initialized directly because initializer delegation is flow-insensitive + // self.identifiers = [] + self.init(identifiers: []) + return + } + let identifiers = dotSeparatedIdentifiers.split( + separator: ".", + omittingEmptySubsequences: false // must preserve empty identifiers, for accurate diagnostics. + ) + try self.init(identifiers) + } + + /// Creates a semantic version pre-release from the given pre-release identifier strings. + /// - Parameter identifiers: The given pre-release identifier strings to create a semantic version pre-release from. + /// - Throws: A `SymbolGraph.SemanticVersionError` instance if any element of `identifiers` is not a valid pre-release identifier string. + internal init(_ identifiers: C) throws where C.Element == S, S.SubSequence == Substring { + self.identifiers = try identifiers.map { + try Identifier($0) + } + } +} + +extension SymbolGraph.SemanticVersion.Prerelease.Identifier { + /// Creates a semantic version pre-release identifier from the given pre-release identifier string. + /// - Parameter identifierString: The given pre-release identifier string to create a semantic version pre-release identifier from. + /// - Throws: A `SymbolGraph.SemanticVersionError` instance if `identifierString` is not a valid pre-release identifier string. + internal init(_ identifierString: S) throws where S.SubSequence == Substring { + guard !identifierString.isEmpty else { + throw SymbolGraph.SemanticVersionError.emptyIdentifier(position: .prerelease) + } + guard identifierString.allSatisfy(\.isAllowedInSemanticVersionIdentifier) else { + throw SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(String(identifierString), position: .prerelease) + } + if identifierString.allSatisfy(\.isNumber) { + // diagnose the identifier as a numeric identifier, if all characters are ASCII digits + guard identifierString.first != "0" || identifierString == "0" else { + throw SymbolGraph.SemanticVersionError.invalidNumericIdentifier( + String(identifierString), + position: .prerelease, + errorKind: .leadingZeros + ) + } + guard let numericIdentifier = UInt(identifierString) else { + if identifierString.isEmpty { + throw SymbolGraph.SemanticVersionError.emptyIdentifier(position: .prerelease) + } else { + throw SymbolGraph.SemanticVersionError.invalidNumericIdentifier( + String(identifierString), + position: .prerelease, + errorKind: .oversizedValue + ) + } + } + self = .numeric(numericIdentifier) + } else { + self = .alphanumeric(String(identifierString)) + } + } +} + +// MARK: - Comparable Conformances + +// Compiler synthesised `Equatable`-conformance is correct here. +extension SymbolGraph.SemanticVersion.Prerelease: Comparable { + internal static func <(lhs: Self, rhs: Self) -> Bool { + guard !lhs.identifiers.isEmpty else { return false } // non-pre-release lhs >= potentially pre-release rhs + guard !rhs.identifiers.isEmpty else { return true } // pre-release lhs < non-pre-release rhs + return lhs.identifiers.lexicographicallyPrecedes(rhs.identifiers) + } +} + +// Compiler synthesised `Equatable`-conformance is correct here. +extension SymbolGraph.SemanticVersion.Prerelease.Identifier: Comparable { + internal static func <(lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case let (.numeric(lhs), .numeric(rhs)): + return lhs < rhs + case let(.alphanumeric(lhs), .alphanumeric(rhs)): + return lhs < rhs + case (.numeric, .alphanumeric): + return true + case (.alphanumeric, .numeric): + return false + } + } +} + +// MARK: CustomStringConvertible Conformances + +extension SymbolGraph.SemanticVersion.Prerelease: CustomStringConvertible { + /// A textual description of the pre-release. + internal var description: String { + identifiers.map(\.description).joined(separator: ".") + } +} + +extension SymbolGraph.SemanticVersion.Prerelease.Identifier: CustomStringConvertible { + /// A textual description of the identifier. + internal var description: String { + switch self { + case .numeric(let identifier): + return identifier.description + case .alphanumeric(let identifier): + return identifier.description + } + } +} + +// MARK: - Validating Identifiers + +extension Character { + /// A Boolean value indicating whether this character is allowed in a semantic version's identifier. + internal var isAllowedInSemanticVersionIdentifier: Bool { + isASCII && (isLetter || isNumber || self == "-") + } + + /// A Boolean value indicating whether this character is allowed in a semantic version's numeric identifier. + internal var isAllowedInSemanticVersionNumericIdentifier: Bool { + isASCII && isNumber + } +} diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion+Comparable.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion+Comparable.swift new file mode 100644 index 0000000..e3492ea --- /dev/null +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion+Comparable.swift @@ -0,0 +1,38 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +extension SymbolGraph.SemanticVersion: Comparable { + // Although `Comparable` inherits from `Equatable`, it does not provide a new default implementation of `==`, but instead uses `Equatable`'s default synthesised implementation. The compiler-synthesised `==`` is composed of [member-wise comparisons](https://github.com/apple/swift-evolution/blob/main/proposals/0185-synthesize-equatable-hashable.md#implementation-details), which leads to a false `false` when 2 semantic versions differ by only their build metadata identifiers, contradicting SemVer 2.0.0's [comparison rules](https://semver.org/#spec-item-10). + /// Returns a Boolean value indicating whether two semantic versions are equal. + /// - Parameters: + /// - lhs: A semantic version to compare. + /// - rhs: Another semantic version to compare. + /// - Returns: `true` if `lhs` and `rhs` are equal; `false` otherwise. + @inlinable + public static func == (lhs: Self, rhs: Self) -> Bool { + !(lhs < rhs) && !(lhs > rhs) + } + + /// Returns a Boolean value indicating whether the first semantic version precedes the second semantic version. + /// - Parameters: + /// - lhs: A semantic version to compare. + /// - rhs: Another semantic version to compare. + /// - Returns: `true` if `lhs` precedes `rhs`; `false` otherwise. + public static func < (lhs: Self, rhs: Self) -> Bool { + let lhsVersionCore = [lhs.major, lhs.minor, lhs.patch] + let rhsVersionCore = [rhs.major, rhs.minor, rhs.patch] + + guard lhsVersionCore == rhsVersionCore else { + return lhsVersionCore.lexicographicallyPrecedes(rhsVersionCore) + } + + return lhs.prerelease < rhs.prerelease // not lexicographically compared + } +} diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion+CustomStringConvertible.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion+CustomStringConvertible.swift new file mode 100644 index 0000000..636d989 --- /dev/null +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion+CustomStringConvertible.swift @@ -0,0 +1,23 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +extension SymbolGraph.SemanticVersion: CustomStringConvertible { + /// A textual description of the semantic version. + public var description: String { + var versionString = "\(major).\(minor).\(patch)" + if !prerelease.identifiers.isEmpty { + versionString += "-\(prerelease)" + } + if !buildMetadataIdentifiers.isEmpty { + versionString += "+" + buildMetadataIdentifiers.joined(separator: ".") + } + return versionString + } +} diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion.swift new file mode 100644 index 0000000..5d3ad4e --- /dev/null +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion.swift @@ -0,0 +1,120 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 - 2022 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +extension SymbolGraph { + /// A [semantic version](https://semver.org). + public struct SemanticVersion { + /// The major version. + public let major: UInt + /// The minor version. + public let minor: UInt + /// The patch version. + public let patch: UInt + /// Dot-separated pre-release identifiers. + public var prereleaseIdentifiers: [String] { prerelease.identifiers.map(\.description) } + /// Dot-separated build metadata identifiers. + public let buildMetadataIdentifiers: [String] + + /// The internal storage of pre-release identifiers. + internal let prerelease: Prerelease + + /// Creates a semantic version with the provided components of a semantic version. + /// - Parameters: + /// - major: The major version number. + /// - minor: The minor version number. + /// - patch: The patch version number. + /// - prerelease: The pre-release information. + /// - buildMetadata: The build metadata. + public init( + major: UInt, + minor: UInt, + patch: UInt, + prerelease: String? = nil, + buildMetadata: String? = nil + ) throws { + self.major = major + self.minor = minor + self.patch = patch + + let prereleaseIdentifiers = prerelease?.split(separator: ".", omittingEmptySubsequences: false) ?? [] + self.prerelease = try Prerelease(prereleaseIdentifiers) + + let buildMetadataIdentifiers = buildMetadata?.split(separator: ".", omittingEmptySubsequences: false).map { String($0) } ?? [] + guard buildMetadataIdentifiers.allSatisfy( { !$0.isEmpty } ) else { + throw SymbolGraph.SemanticVersionError.emptyIdentifier(position: .buildMetadata) + } + try buildMetadataIdentifiers.forEach { + guard $0.allSatisfy( { $0.isASCII && ( $0.isLetter || $0.isNumber || $0 == "-" ) } ) else { + throw SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier($0, position: .buildMetadata) + } + } + self.buildMetadataIdentifiers = buildMetadataIdentifiers + } + + @available(*, deprecated, renamed: "init(_:_:_:prerelease:buildMetadata:)") + @_disfavoredOverload + public init( + major: Int, + minor: Int, + patch: Int, + prerelease: String? = nil, + buildMetadata: String? = nil + ) { + try! self.init( + major: UInt(major), + minor: UInt(minor), + patch: UInt(patch), + prerelease: prerelease, + buildMetadata: buildMetadata + ) + } + } +} + +// MARK: - Inspecting the Semantics + +extension SymbolGraph.SemanticVersion { + /// A Boolean value indicating whether the version is for a pre-release. + public var denotesPrerelease: Bool { !prerelease.identifiers.isEmpty } + + /// A Boolean value indicating whether the version is for a stable release. + public var denotesStableRelease: Bool { major > 0 && !denotesPrerelease } + + /// Returns a Boolean value indicating whether a release with this version can introduce source-breaking changes from that with the given other version. + /// - Parameter other: The older version a release with which to check if a release with the current version is allowed to source-break from. + /// - Returns: A Boolean value indicating whether a release with this version can introduce source-breaking changes from that with `other`. + public func denotesSourceBreakableRelease(fromThatWith other: Self) -> Bool { + self > other && ( + self.major != other.major || + self.major == 0 || // When self.major == 0, other.major must also == 0 here. + (self.denotesPrerelease || other.denotesPrerelease) + ) + } +} + +// MARK: - Creating a Version Semantically + +extension SymbolGraph.SemanticVersion { + /// The version that denotes the initial stable release. + public static var initialStableReleaseVersion: Self { + try! Self(major: 1, minor: 0, patch: 0) + } + + /// Returns the version denoting the next major release that comes after the release denoted with the given version. + /// - Parameter version: The version after which the version that denotes the next major release may come. + /// - Returns: The version denoting the next major release that comes after the release denoted with `version`. + public func nextMajorReleaseVersion(from version: Self) -> Self { + if version.denotesPrerelease && version.major > 0 && version.minor == 0 && version.patch == 0 { + return try! Self(major: version.major, minor: 0, patch: 0) + } else { + return try! Self(major: version.major + 1, minor: 0, patch: 0) + } + } +} diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersionError.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersionError.swift new file mode 100644 index 0000000..82ebcd3 --- /dev/null +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersionError.swift @@ -0,0 +1,120 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +extension SymbolGraph { + /// An error that occurs during the creation of a semantic version. + public enum SemanticVersionError: Error, CustomStringConvertible { + /// The identifier at the given position is empty. + /// - Parameter position: The empty identifier's position in the semantic version. + case emptyIdentifier(position: IdentifierPosition) + /// The identifier at the given position contains invalid character(s). + /// - Parameters: + /// - identifier: The identifier that contains invalid character(s). + /// - position: The given identifier's position in the semantic version. + case invalidCharacterInIdentifier(_ identifier: String, position: AlphanumericIdentifierPosition) + /// The numeric identifier at the given position is invalid for the given reason. + /// - Parameters: + /// - identifier: The invalid numeric identifier. + /// - position: The given numeric identifier's position in the semantic version. + /// - errorKind: The reason why the given numeric identifier is invalid. + case invalidNumericIdentifier(_ identifier: String, position: NumericIdentifierPosition, errorKind: NumericIdentifierErrorKind) + /// The version core contains an invalid number of Identifiers. + /// - Parameter identifiers: The version core identifiers in the version string. + case invalidVersionCoreIdentifierCount(identifiers: [String]) + + /// A position of an identifier in a semantic version. + public enum IdentifierPosition: String, CustomStringConvertible { + /// The major version number position in a semantic version. + case major = "major version number" + /// The minor version number position in a semantic version. + case minor = "minor version number" + /// The patch version number position in a semantic version. + case patch = "patch version number" + /// The pre-release position in a semantic version. + case prerelease = "pre-release" + /// The build-metadata position in a semantic version. + case buildMetadata = "build metadata" + + /// A textual description of the identifier's position. + public var description: String { + self.rawValue + } + } + + /// A position of an alpha-numeric identifier in a semantic version. + public enum AlphanumericIdentifierPosition: String, CustomStringConvertible { + /// The pre-release position in a semantic version. + case prerelease = "pre-release" // This case is backed by "pre-release" instead of "pre-release alpha-numeric", because it makes mores sense to state it as a general rule for pre-release identifiers instead of just pre-release alpha-numeric identifiers. + /// The build-metadata position in a semantic version. + case buildMetadata = "build metadata" + + /// A textual description of the alpha-numeric identifier's position. + public var description: String { + self.rawValue + } + } + + /// A position of a numeric identifier in a semantic version. + public enum NumericIdentifierPosition: String, CustomStringConvertible { + /// The major version number position in a semantic version. + case major = "major version number" + /// The minor version number position in a semantic version. + case minor = "minor version number" + /// The patch version number position in a semantic version. + case patch = "patch version number" + /// The pre-release position in a semantic version. + case prerelease = "pre-release numeric" + + /// A textual description of the numeric identifier's position. + public var description: String { + self.rawValue + } + } + + /// A reason why a numeric identifier is invalid. + public enum NumericIdentifierErrorKind { + /// The numeric identifier contains leading "0" characters. + case leadingZeros + /// The numeric identifier contains non-numeric characters. + case nonNumericCharacter + /// The numeric identifier is too large to be representable by `UInt`. + case oversizedValue + } + + // this description follows [the "grammar and phrasing" section of Swift's diagnostics guidelines](https://github.com/apple/swift/blob/d1bb98b11ede375a1cee739f964b7d23b6657aaf/docs/Diagnostics.md#grammar-and-phrasing) + /// A textual description of the `SymbolGraph.SemanticVersionError` instance. + public var description: String { + switch self { + case let .emptyIdentifier(position): + return "semantic version \(position) identifier cannot be empty" + case let .invalidCharacterInIdentifier(identifier, position): + return "semantic version \(position) identifier '\(identifier)' cannot contain characters other than ASCII alphanumerics and hyphen-minus ([0-9A-Za-z-])" + case let .invalidNumericIdentifier(identifier, position, errorKind): + let fault: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 + switch errorKind { + case .leadingZeros: + return "contain leading '0'" + case .nonNumericCharacter: + return "contain non-numeric characters" + case .oversizedValue: + return "be larger than 'UInt.max'" + } + }() + return "semantic version \(position) identifier '\(identifier)' cannot \(fault)" + case let .invalidVersionCoreIdentifierCount(identifiers): + return """ + semantic version must contain exactly 3 version core identifiers; \ + \(identifiers.count) given\(identifiers.isEmpty ? "" : " : ")\ + \(identifiers.map { "'\($0)'" } .joined(separator: ", ")) + """ + } + } + } +} diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/SematicVerion+Codable.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SematicVerion+Codable.swift new file mode 100644 index 0000000..c23d420 --- /dev/null +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SematicVerion+Codable.swift @@ -0,0 +1,53 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +extension SymbolGraph.SemanticVersion: Codable { + internal enum CodingKeys: String, CodingKey { + case major + case minor + case patch + case prerelease + case buildMetadata + } + + /// Creates a semantic version by decoding from the given decoder. + /// - Parameter decoder: The decoder to read data from. + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let major = try container.decode(UInt.self, forKey: .major) + let minor = try container.decodeIfPresent(UInt.self, forKey: .minor) ?? 0 + let patch = try container.decodeIfPresent(UInt.self, forKey: .patch) ?? 0 + let prerelease = try container.decodeIfPresent(String.self, forKey: .prerelease) + let buildMetadata = try container.decodeIfPresent(String.self, forKey: .buildMetadata) + try self.init( + major: major, + minor: minor, + patch: patch, + prerelease: prerelease, + buildMetadata: buildMetadata + ) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(major, forKey: .major) + try container.encode(minor, forKey: .minor) + try container.encode(patch, forKey: .patch) + + if denotesPrerelease { + try container.encode(prerelease.description, forKey: .prerelease) + } + + if !buildMetadataIdentifiers.isEmpty { + try container.encode(buildMetadataIdentifiers.joined(separator: "."), forKey: .buildMetadata) + } + } +} diff --git a/Tests/SymbolKitTests/SymbolGraph/LineList/SemanticVersionTests.swift b/Tests/SymbolKitTests/SymbolGraph/LineList/SemanticVersionTests.swift deleted file mode 100644 index a0484ff..0000000 --- a/Tests/SymbolKitTests/SymbolGraph/LineList/SemanticVersionTests.swift +++ /dev/null @@ -1,26 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021 Apple Inc. and the Swift project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See https://swift.org/LICENSE.txt for license information - See https://swift.org/CONTRIBUTORS.txt for Swift project authors -*/ - -import XCTest -@testable import SymbolKit - -class SemanticVersionTests: XCTestCase { - typealias SemanticVersion = SymbolGraph.SemanticVersion - - func testVersionInit() { - let version = SemanticVersion(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "enableX") - - XCTAssertEqual(version.major, 1) - XCTAssertEqual(version.minor, 2) - XCTAssertEqual(version.patch, 3) - XCTAssertEqual(version.prerelease, "beta") - XCTAssertEqual(version.buildMetadata, "enableX") - } -} diff --git a/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/CodingTests.swift b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/CodingTests.swift new file mode 100644 index 0000000..fe94d8c --- /dev/null +++ b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/CodingTests.swift @@ -0,0 +1,94 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +import XCTest +@testable import SymbolKit + +final class CodingTests:XCTestCase { + func testEncodingToJSON() throws { + func assertEncoding(_ version: SymbolGraph.SemanticVersion, to expectedJSONObject: String) throws { + let jsonEncoder = JSONEncoder() + jsonEncoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let encodedVersion = try jsonEncoder.encode(version) + XCTAssertEqual( + String(data: encodedVersion, encoding: .utf8), + // Because Semantic Versioning 2.0.0 does not allow whitespace in identifiers, we can remove whitespace from the expected JSON object string. + expectedJSONObject.filter { !$0.isWhitespace } + ) + } + + let testCases: [(version: SymbolGraph.SemanticVersion, expectedJSONObject: String)] = [ + (try .init(major: 0, minor: 0, patch: 0), #"{"major":0,"minor":0,"patch":0}"#), + (try .init(major: 6, minor: 9, patch: 42), #"{"major":6,"minor":9,"patch":42}"#), + (try .init(major: 1, minor: 2, patch: 3, prerelease: "beta-004.5"), #"{"major":1,"minor":2,"patch":3,"prerelease":"beta-004.5"}"#), + (try .init(major: 6, minor: 7, patch: 8, buildMetadata: "2020-02-02.sha256-C951120B9D2E83CCCF8F51477FF53943447D56899185E0E0DC7C3EDFBA19CD60"), #"{"buildMetadata":"2020-02-02.sha256-C951120B9D2E83CCCF8F51477FF53943447D56899185E0E0DC7C3EDFBA19CD60","major":6,"minor":7,"patch":8}"#), + (try .init(major: 9, minor: 10, patch: 11, prerelease: "alpha42.release-candidate-1", buildMetadata: "md5-8981EDE66EBF3F83819F709A73B22BBA"), #"{"buildMetadata":"md5-8981EDE66EBF3F83819F709A73B22BBA","major":9,"minor":10,"patch":11,"prerelease":"alpha42.release-candidate-1"}"#) + ] + + try testCases.forEach { try assertEncoding($0.version, to: $0.expectedJSONObject) } + } + + func testDecodingFromJSON() throws { + func assertDecoding(_ jsonObject: String, to expectedVersion: SymbolGraph.SemanticVersion) throws { + let jsonDecoder = JSONDecoder() + let jsonObjectData = Data(jsonObject.utf8) + let decodedVersion = try jsonDecoder.decode(SymbolGraph.SemanticVersion.self, from: jsonObjectData) + XCTAssertEqual(decodedVersion, expectedVersion) + } + + let validCases: [(jsonObject: String, expectedVersion: SymbolGraph.SemanticVersion)] = [ + (#"{"major":0,"minor":0,"patch":0}"#, try .init(major: 0, minor: 0, patch: 0)), + (#"{"major":6,"minor":7,"patch":42}"#, try .init(major: 6, minor: 7, patch: 42)), + (#"{"major":1,"minor":2,"patch":3,"prerelease":"-41ph4-1337.7.0"}"#, try .init(major: 1, minor: 2, patch: 3, prerelease: "-41ph4-1337.7.0")), + (#"{"major":8,"minor":9,"patch":10,"buildMetadata":"LTS.sha256-076F19B8ECCD0B911C407C4881DE9D0C1D8128B631CF52DBB7BB96C11EA5D5EA"}"#, try .init(major: 8, minor: 9, patch: 10, buildMetadata: "LTS.sha256-076F19B8ECCD0B911C407C4881DE9D0C1D8128B631CF52DBB7BB96C11EA5D5EA")), + (#"{"major":11,"minor":12,"patch":13,"prerelease":"beta.golden-master.42","buildMetadata":"md5-5FDD8FAC22BF07D9010F7F482745F6D5"}"#, try .init(major: 11, minor: 12, patch: 13, prerelease: "beta.golden-master.42", buildMetadata: "md5-5FDD8FAC22BF07D9010F7F482745F6D5")) + ] + + func assertSemanticVersionErrorThrownFromDecoding(_ jsonObject: String) { + let jsonDecoder = JSONDecoder() + let jsonObjectData = Data(jsonObject.utf8) + XCTAssertThrowsError(try jsonDecoder.decode(SymbolGraph.SemanticVersion.self, from: jsonObjectData)) { error in + XCTAssertTrue(error is SymbolGraph.SemanticVersionError) + } + } + + try validCases.forEach { try assertDecoding($0.jsonObject, to: $0.expectedVersion) } + + let invalidCases = [ + #"{"major":4,"minor":5,"patch":6,"prerelease":""}"#, + #"{"major":7,"minor":8,"patch":9,"prerelease":" "}"#, + #"{"major":0,"minor":1,"patch":2,"prerelease":"."}"#, + #"{"major":3,"minor":4,"patch":5,"prerelease":".."}"#, + #"{"major":6,"minor":7,"patch":8,"prerelease":".a"}"#, + #"{"major":9,"minor":0,"patch":1,"prerelease":"b."}"#, + #"{"major":2,"minor":3,"patch":4,"prerelease":"00"}"#, + #"{"major":5,"minor":6,"patch":7,"prerelease":"01"}"#, + #"{"major":8,"minor":9,"patch":0,"prerelease":" 0"}"#, + #"{"major":9,"minor":8,"patch":7,"prerelease":"@#"}"#, + #"{"major":6,"minor":5,"patch":4,"prerelease":"œ∑"}"#, + #"{"major":3,"minor":2,"patch":1,"prerelease":"+"}"#, + + #"{"major":0,"minor":9,"patch":8,"buildMetadata":""}"#, + #"{"major":7,"minor":6,"patch":5,"buildMetadata":" "}"#, + #"{"major":4,"minor":3,"patch":2,"buildMetadata":"+"}"#, + #"{"major":1,"minor":0,"patch":9,"buildMetadata":"."}"#, + #"{"major":8,"minor":7,"patch":6,"buildMetadata":".."}"#, + #"{"major":5,"minor":4,"patch":3,"buildMetadata":".a"}"#, + #"{"major":2,"minor":1,"patch":0,"buildMetadata":"b."}"#, + #"{"major":0,"minor":1,"patch":1,"buildMetadata":" 0"}"#, + #"{"major":2,"minor":3,"patch":5,"buildMetadata":"@#"}"#, + #"{"major":8,"minor":1,"patch":3,"buildMetadata":"¥ø"}"#, + + #"{"major":2,"minor":1,"patch":3,"prerelease":"","buildMetadata":""}"#, + ] + + invalidCases.forEach { assertSemanticVersionErrorThrownFromDecoding($0) } + } +} diff --git a/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/ErrorTests.swift b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/ErrorTests.swift new file mode 100644 index 0000000..b454129 --- /dev/null +++ b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/ErrorTests.swift @@ -0,0 +1,376 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +import XCTest +@testable import SymbolKit + +final class ErrorTests: XCTestCase { + func testEmptyIdentifiers() { + // MARK: pre-release + + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try .init(major: 0, minor: 1, patch: 2, prerelease: "")) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try .init(major: 1, minor: 2, patch: 3, prerelease: ".")) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try .init(major: 2, minor: 3, patch: 4, prerelease: "alpha.")) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try .init(major: 3, minor: 4, patch: 5, prerelease: ".beta")) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try .init(major: 4, minor: 5, patch: 6, prerelease: "123.")) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try .init(major: 5, minor: 6, patch: 7, prerelease: ".456")) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try .init(major: 6, minor: 7, patch: 8, prerelease: "y2k.")) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try .init(major: 7, minor: 8, patch: 9, prerelease: ".mp3")) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":9,"minor":7,"patch":5,"prerelease":""}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":8,"minor":6,"patch":4,"prerelease":"."}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":7,"minor":5,"patch":3,"prerelease":"gold."}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":6,"minor":4,"patch":2,"prerelease":".master"}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":5,"minor":3,"patch":1,"prerelease":"0."}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":4,"minor":2,"patch":0,"prerelease":".1"}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":3,"minor":1,"patch":9,"prerelease":"2001odyssey."}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":2,"minor":0,"patch":8,"prerelease":".av1"}"#.utf8))) + + // MARK: build metadata + + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try .init(major: 0, minor: 1, patch: 2, buildMetadata: "")) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try .init(major: 1, minor: 2, patch: 3, buildMetadata: ".")) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try .init(major: 2, minor: 3, patch: 4, buildMetadata: "alpha.")) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try .init(major: 3, minor: 4, patch: 5, buildMetadata: ".beta")) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try .init(major: 4, minor: 5, patch: 6, buildMetadata: "123.")) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try .init(major: 5, minor: 6, patch: 7, buildMetadata: ".456")) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try .init(major: 6, minor: 7, patch: 8, buildMetadata: "y2k.")) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try .init(major: 7, minor: 8, patch: 9, buildMetadata: ".mp3")) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":9,"minor":7,"patch":5,"buildMetadata":""}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":8,"minor":6,"patch":4,"buildMetadata":"."}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":7,"minor":5,"patch":3,"buildMetadata":"gold."}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":6,"minor":4,"patch":2,"buildMetadata":".master"}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":5,"minor":3,"patch":1,"buildMetadata":"0."}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":4,"minor":2,"patch":0,"buildMetadata":".1"}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":3,"minor":1,"patch":9,"buildMetadata":"2001odyssey."}"#.utf8))) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":2,"minor":0,"patch":8,"buildMetadata":".av1"}"#.utf8))) + } + + func testEmptyIdentifierDiagnosticPrecedence() { + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try .init(major: 1, minor: 2, patch: 3, prerelease: "", buildMetadata: "")) + assertThrowingEmptyIdentifierError(atPosition: .prerelease, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":1,"minor":2,"patch":3,"prerelease":"","buildMetadata":""}"#.utf8))) + + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try .init(major: 1, minor: 2, patch: 3, prerelease: "4", buildMetadata: "")) + assertThrowingEmptyIdentifierError(atPosition: .buildMetadata, whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":1,"minor":2,"patch":3,"prerelease":"4","buildMetadata":""}"#.utf8))) + } + + func testNonAlphanumericCharacters() { + // MARK: pre-release + + assertThrowingNonAlphanumericCharacterError(atPosition: .prerelease, inIdentifier: "😛", whenEvaluating: try .init(major: 9, minor: 8, patch: 7, prerelease: "😛")) + assertThrowingNonAlphanumericCharacterError(atPosition: .prerelease, inIdentifier: "😝alpha", whenEvaluating: try .init(major: 6, minor: 5, patch: 4, prerelease: "😝alpha.beta")) + assertThrowingNonAlphanumericCharacterError(atPosition: .prerelease, inIdentifier: "beta😜", whenEvaluating: try .init(major: 3, minor: 2, patch: 1, prerelease: "alpha.beta😜")) + assertThrowingNonAlphanumericCharacterError(atPosition: .prerelease, inIdentifier: "pre🤪release", whenEvaluating: try .init(major: 0, minor: 9, patch: 8, prerelease: "pre🤪release.un🤨stable")) + assertThrowingNonAlphanumericCharacterError(atPosition: .prerelease, inIdentifier: "🥳", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":5,"minor":4,"patch":3,"prerelease":"🥳"}"#.utf8))) + assertThrowingNonAlphanumericCharacterError(atPosition: .prerelease, inIdentifier: "😏123", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":2,"minor":1,"patch":0,"prerelease":"😏123.456"}"#.utf8))) + assertThrowingNonAlphanumericCharacterError(atPosition: .prerelease, inIdentifier: "456😒", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":1,"minor":2,"patch":3,"prerelease":"123.456😒"}"#.utf8))) + assertThrowingNonAlphanumericCharacterError(atPosition: .prerelease, inIdentifier: "13😞37", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":4,"minor":5,"patch":6,"prerelease":"13😞37.le😔et"}"#.utf8))) + + // MARK: build metadata + + assertThrowingNonAlphanumericCharacterError(atPosition: .buildMetadata, inIdentifier: "😟", whenEvaluating: try .init(major: 7, minor: 8, patch: 9, buildMetadata: "😟")) + assertThrowingNonAlphanumericCharacterError(atPosition: .buildMetadata, inIdentifier: "😕abcd", whenEvaluating: try .init(major: 0, minor: 1, patch: 2, buildMetadata: "😕abcd.efgh")) + assertThrowingNonAlphanumericCharacterError(atPosition: .buildMetadata, inIdentifier: "mnop🙁", whenEvaluating: try .init(major: 3, minor: 4, patch: 5, buildMetadata: "ijkl.mnop🙁")) + assertThrowingNonAlphanumericCharacterError(atPosition: .buildMetadata, inIdentifier: "qr☹️st", whenEvaluating: try .init(major: 6, minor: 7, patch: 8, buildMetadata: "qr☹️st.uv😣wx")) + assertThrowingNonAlphanumericCharacterError(atPosition: .buildMetadata, inIdentifier: "😭", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":9,"minor":8,"patch":7,"buildMetadata":"😭"}"#.utf8))) + assertThrowingNonAlphanumericCharacterError(atPosition: .buildMetadata, inIdentifier: "😤1a2", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":6,"minor":5,"patch":4,"buildMetadata":"😤1a2.b3c"}"#.utf8))) + assertThrowingNonAlphanumericCharacterError(atPosition: .buildMetadata, inIdentifier: "e6f😠", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":3,"minor":2,"patch":1,"buildMetadata":"4d5.e6f😠"}"#.utf8))) + assertThrowingNonAlphanumericCharacterError(atPosition: .buildMetadata, inIdentifier: "7g8😡h9i", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":0,"minor":9,"patch":8,"buildMetadata":"7g8😡h9i.10j🤬11k"}"#.utf8))) + } + + func testNonNumericCharacters() { + // MARK: pre-release + + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 9, minor: 8, patch: 7, prerelease: "abc123")) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 6, minor: 5, patch: 4, prerelease: "456def")) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 3, minor: 2, patch: 1, prerelease: "7g8h9i")) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":1,"minor":0,"patch":9,"prerelease":"stu901"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":8,"minor":7,"patch":6,"prerelease":"234vwx"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":5,"minor":4,"patch":3,"prerelease":"5y6z7a"}"#.utf8))) + + // MARK: build metadata + + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 2, minor: 1, patch: 0, buildMetadata: "bcd890")) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 9, minor: 8, patch: 7, buildMetadata: "123efg")) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 6, minor: 5, patch: 4, buildMetadata: "h4i5j6")) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":4,"minor":3,"patch":2,"buildMetadata":"tuv678"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":1,"minor":0,"patch":9,"buildMetadata":"901wxy"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":8,"minor":7,"patch":6,"buildMetadata":"z2a3b4"}"#.utf8))) + } + + func testLeadingZeros() { + // MARK: pre-release + + assertThrowingLeadingZerosError(atPosition: .prerelease, inIdentifier: "0246", whenEvaluating: try .init(major: 1, minor: 3, patch: 5, prerelease: "0246")) + assertThrowingLeadingZerosError(atPosition: .prerelease, inIdentifier: "0068", whenEvaluating: try .init(major: 3, minor: 5, patch: 7, prerelease: "0068")) + assertThrowingLeadingZerosError(atPosition: .prerelease, inIdentifier: "0000", whenEvaluating: try .init(major: 5, minor: 7, patch: 9, prerelease: "0000")) + assertThrowingLeadingZerosError(atPosition: .prerelease, inIdentifier: "0654", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":9,"minor":8,"patch":7,"prerelease":"0654"}"#.utf8))) + assertThrowingLeadingZerosError(atPosition: .prerelease, inIdentifier: "0065", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":9,"minor":8,"patch":7,"prerelease":"0065"}"#.utf8))) + assertThrowingLeadingZerosError(atPosition: .prerelease, inIdentifier: "0000", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":9,"minor":8,"patch":7,"prerelease":"0000"}"#.utf8))) + + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 2, minor: 3, patch: 4, prerelease: "0")) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":8,"minor":9,"patch":1,"prelease":"0"}"#.utf8))) + + // MARK: build metadata + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 2, minor: 3, patch: 4, buildMetadata: "0567")) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 8, minor: 9, patch: 1, buildMetadata: "0023")) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 4, minor: 5, patch: 6, buildMetadata: "0000")) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 7, minor: 8, patch: 9, buildMetadata: "0")) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":9,"minor":1,"patch":2,"buildMetadata":"0345"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":6,"minor":7,"patch":8,"buildMetadata":"0091"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":2,"minor":3,"patch":4,"buildMetadata":"0000"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":5,"minor":6,"patch":7,"buildMetadata":"0"}"#.utf8))) + } + + func testOversizedNumericValues() { + func sum(_ summand1: String, _ summand2: String) -> Substring { + let paddedSummandLength = max(summand1.count, summand2.count) + 1 + let paddedSummand1 = zeroPadded(summand1, toLength: paddedSummandLength) + let paddedSummand2 = zeroPadded(summand2, toLength: paddedSummandLength) + + var result: [Character] = [] + result.reserveCapacity(paddedSummandLength) + + var carry: Int8 = 0 + + for (digit1, digit2) in zip(paddedSummand1.reversed(), paddedSummand2.reversed()) { + let digit1 = Int8(String(digit1))! + let digit2 = Int8(String(digit2))! + let columnSum = digit1 + digit2 + carry + carry = columnSum > 9 ? 1 : 0 + result.append(String(columnSum).last!) + } + + return Substring(result[...result.lastIndex(where: { $0 != "0" })!].reversed()) + + func zeroPadded(_ number: String, toLength paddedLength: Int) -> [Character] { + let paddingLength = paddedLength - number.count + var paddedNumber = [Character](repeating: "0", count: paddingLength) + paddedNumber.reserveCapacity(paddedLength) + paddedNumber.append(contentsOf: number) + return paddedNumber + } + } + + // MARK: infrastructure sanity check + + XCTAssertEqual(sum("123", "456"), "579") + XCTAssertEqual(sum("999", "999"), "1998") + XCTAssertEqual(sum("4545", "4545"), "9090") + XCTAssertEqual(sum("123456789", "98765"), "123555554") + XCTAssertEqual(sum("111111111111111111111111111111", "111111111111111111111111111111"), "222222222222222222222222222222") // 30 digits in each + XCTAssertEqual(sum("111111111111111111111111111111", "000000000011111111112222222222"), "111111111122222222223333333333") + XCTAssertEqual(sum("999999999999999999999999999999", "999999999999999999999999999999"), "1999999999999999999999999999998") // 30 digits in each + XCTAssertEqual(sum("\(UInt64.max)", "1"), "18446744073709551616") + XCTAssertEqual(sum("\(UInt64.max)", "\(Int32.max)"), "18446744075857035262") + XCTAssertEqual(sum("\(UInt64.max)", "\(UInt64.max)"), "36893488147419103230") + + // MARK: pre-release + + assertThrowingOversizedValueError(atPosition: .prerelease, inIdentifier: sum("\(UInt.max)", "1"), whenEvaluating: try .init(major: 1, minor: 3, patch: 5, prerelease: String(sum("\(UInt.max)", "1")))) + assertThrowingOversizedValueError(atPosition: .prerelease, inIdentifier: "8\(UInt.max)", whenEvaluating: try .init(major: 2, minor: 4, patch: 6, prerelease: "8\(UInt.max)")) + assertThrowingOversizedValueError(atPosition: .prerelease, inIdentifier: "\(UInt.max)\(UInt.max)", whenEvaluating: try .init(major: 3, minor: 5, patch: 7, prerelease: "\(UInt.max)\(UInt.max)")) + assertThrowingOversizedValueError(atPosition: .prerelease, inIdentifier: sum("\(UInt.max)", "1"), whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":9,"minor":8,"patch":7,"prerelease":"\#(sum("\(UInt.max)", "1"))"}"#.utf8))) + assertThrowingOversizedValueError(atPosition: .prerelease, inIdentifier: "3\(UInt.max)", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":6,"minor":5,"patch":4,"prerelease":"3\#(UInt.max)"}"#.utf8))) + assertThrowingOversizedValueError(atPosition: .prerelease, inIdentifier: "\(UInt.max)\(UInt64.max)", whenEvaluating: try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":2,"minor":1,"patch":0,"prerelease":"\#(UInt.max)\#(UInt64.max)"}"#.utf8))) + + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 1, minor: 2, patch: 3, prerelease: "\(UInt.max)")) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":7,"minor":8,"patch":9,"prerelease":"\#(UInt.max)"}"#.utf8))) + + // MARK: build metadata + + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 1, minor: 2, patch: 3, buildMetadata: "\(UInt.max)")) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 4, minor: 5, patch: 6, buildMetadata: String(sum("\(UInt.max)", "1")))) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 7, minor: 8, patch: 9, buildMetadata: "\(UInt.max)0")) + XCTAssertNoThrow(try SymbolGraph.SemanticVersion(major: 1, minor: 2, patch: 3, buildMetadata: "\(UInt.max)\(Int.max)")) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":7,"minor":8,"patch":9,"buildMetadata":"\#(UInt.max)"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":0,"minor":1,"patch":2,"buildMetadata":"\#(sum("\(UInt.max)", "1"))"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":3,"minor":4,"patch":5,"buildMetadata":"\#(UInt.max)6"}"#.utf8))) + XCTAssertNoThrow(try JSONDecoder().decode(SymbolGraph.SemanticVersion.self, from: Data(#"{"major":7,"minor":8,"patch":9,"buildMetadata":"\#(UInt.max)\#(UInt.max)"}"#.utf8))) + } + + // TODO: Add tests for the precedence of error-throwing. + + func assertThrowingEmptyIdentifierError( + atPosition position: SymbolGraph.SemanticVersionError.IdentifierPosition, + whenEvaluating expression: @autoclosure () throws -> SymbolGraph.SemanticVersion + ) { + let positionString: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 + switch position { + case .major: return "major" + case .minor: return "minor" + case .patch: return "patch" + case .prerelease: return "prerelease" + case .buildMetadata: return "buildMetadata" + } + }() + XCTAssertThrowsError( + try expression(), + "'SymbolGraph.SemanticVersionError.emptyIdentifier(position: .\(positionString))' should've been thrown, but no error is thrown" + ) { error in + guard let error = error as? SymbolGraph.SemanticVersionError, case .emptyIdentifier(position: position) = error else { + XCTFail(#"'SymbolGraph.SemanticVersionError.emptyIdentifier(position: .\#(positionString))' should've been thrown, but a different error is thrown instead; error description: "\#(error)""#) + return + } + let positionDescription: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 + switch position { + case .major: return "major version number" + case .minor: return "minor version number" + case .patch: return "patch version number" + case .prerelease: return "pre-release" + case .buildMetadata: return "build metadata" + } + }() + XCTAssertEqual( + error.description, + "semantic version \(positionDescription) identifier cannot be empty" + ) + } + } + + func assertThrowingNonAlphanumericCharacterError( + atPosition position: SymbolGraph.SemanticVersionError.AlphanumericIdentifierPosition, + inIdentifier identifier: Substring, + whenEvaluating expression: @autoclosure () throws -> SymbolGraph.SemanticVersion + ) { + let positionString: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 + switch position { + case .prerelease: return "prerelease" + case .buildMetadata: return "buildMetadata" + } + }() + XCTAssertThrowsError( + try expression(), + "'SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(\(identifier), position: .\(positionString))' should've been thrown, but no error is thrown" + ) { error in + guard let error = error as? SymbolGraph.SemanticVersionError, case .invalidCharacterInIdentifier(identifier, position: position) = error else { + XCTFail((#"'SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(\#(identifier), position: .\#(positionString))' should've been thrown, but a different error is thrown instead; error description: "\#(error)""#)) + return + } + let positionDescription: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 + switch position { + case .prerelease: return "pre-release" + case .buildMetadata: return "build metadata" + } + }() + XCTAssertEqual( + error.description, + "semantic version \(positionDescription) identifier '\(identifier)' cannot contain characters other than ASCII alphanumerics and hyphen-minus ([0-9A-Za-z-])" + ) + } + } + + func assertThrowingNonNumericCharacterError( + atPosition position: SymbolGraph.SemanticVersionError.NumericIdentifierPosition, + inIdentifier identifier: Substring, + whenEvaluating expression: @autoclosure () throws -> SymbolGraph.SemanticVersion + ) { + let positionString: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 + switch position { + case .major: return "major" + case .minor: return "minor" + case .patch: return "patch" + case .prerelease: XCTFail("pre-release identifier with non-numeric characters should be regarded as alpha-numeric identifier"); return "prerelease" + } + }() + XCTAssertThrowsError( + try expression(), + "'SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(\(identifier), position: .\(positionString))' should've been thrown, but no error is thrown" + ) { error in + guard let error = error as? SymbolGraph.SemanticVersionError, case .invalidNumericIdentifier(identifier, position: position, errorKind: .nonNumericCharacter) = error else { + XCTFail((#"'SymbolGraph.SemanticVersionError.invalidNumericIdentifier(\#(identifier), position: \#(positionString), errorKind: .nonNumericCharacter)' should've been thrown, but a different error is thrown instead; error description: "\#(error)""#)) + return + } + let positionDescription: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 + switch position { + case .major: return "major version number" + case .minor: return "minor version number" + case .patch: return "patch version number" + case .prerelease: XCTFail("pre-release identifier with non-numeric characters should be regarded as alpha-numeric identifier"); return "pre-release numeric" + } + }() + XCTAssertEqual( + error.description, + "semantic version \(positionDescription) identifier '\(identifier)' cannot contain non-numeric characters" + ) + } + } + + func assertThrowingLeadingZerosError( + atPosition position: SymbolGraph.SemanticVersionError.NumericIdentifierPosition, + inIdentifier identifier: Substring, + whenEvaluating expression: @autoclosure () throws -> SymbolGraph.SemanticVersion + ) { + let positionString: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 + switch position { + case .major: return "major" + case .minor: return "minor" + case .patch: return "patch" + case .prerelease: return "prerelease" + } + }() + XCTAssertThrowsError( + try expression(), + "'SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(\(identifier), position: .\(positionString))' should've been thrown, but no error is thrown" + ) { error in + guard let error = error as? SymbolGraph.SemanticVersionError, case .invalidNumericIdentifier(identifier, position: position, errorKind: .leadingZeros) = error else { + XCTFail((#"'SymbolGraph.SemanticVersionError.invalidNumericIdentifier(\#(identifier), position: \#(positionString), errorKind: .leadingZeros)' should've been thrown, but a different error is thrown instead; error description: "\#(error)""#)) + return + } + let positionDescription: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 + switch position { + case .major: return "major version number" + case .minor: return "minor version number" + case .patch: return "patch version number" + case .prerelease: return "pre-release numeric" + } + }() + XCTAssertEqual( + error.description, + "semantic version \(positionDescription) identifier '\(identifier)' cannot contain leading '0'" + ) + } + } + + func assertThrowingOversizedValueError( + atPosition position: SymbolGraph.SemanticVersionError.NumericIdentifierPosition, + inIdentifier identifier: Substring, + whenEvaluating expression: @autoclosure () throws -> SymbolGraph.SemanticVersion + ) { + let positionString: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 + switch position { + case .major: return "major" + case .minor: return "minor" + case .patch: return "patch" + case .prerelease: return "prerelease" + } + }() + XCTAssertThrowsError( + try expression(), + "'SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(\(identifier), position: .\(positionString))' should've been thrown, but no error is thrown" + ) { error in + guard let error = error as? SymbolGraph.SemanticVersionError, case .invalidNumericIdentifier(identifier, position: position, errorKind: .oversizedValue) = error else { + XCTFail((#"'SymbolGraph.SemanticVersionError.invalidNumericIdentifier(\#(identifier), position: \#(positionString), errorKind: .oversizedValue)' should've been thrown, but a different error is thrown instead; error description: "\#(error)""#)) + return + } + let positionDescription: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 + switch position { + case .major: return "major version number" + case .minor: return "minor version number" + case .patch: return "patch version number" + case .prerelease: return "pre-release numeric" + } + }() + XCTAssertEqual( + error.description, + "semantic version \(positionDescription) identifier '\(identifier)' cannot be larger than 'UInt.max'" + ) + } + } +} diff --git a/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/MainInitializerTests.swift b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/MainInitializerTests.swift new file mode 100644 index 0000000..0d151a5 --- /dev/null +++ b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/MainInitializerTests.swift @@ -0,0 +1,117 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +import XCTest +@testable import SymbolKit + +final class MainInitializationTests: XCTestCase { + func testInitializationFromComponents() throws { + + // MARK: primary public properties + + let version1 = try SymbolGraph.SemanticVersion( + major: 0, minor: 0, patch: 0, + prerelease: nil, buildMetadata: nil + ) + XCTAssertEqual(version1.major, 0) + XCTAssertEqual(version1.minor, 0) + XCTAssertEqual(version1.patch, 0) + XCTAssertEqual(version1.prereleaseIdentifiers, []) + XCTAssertEqual(version1.buildMetadataIdentifiers, []) + + let version2 = try SymbolGraph.SemanticVersion( + major: 1, minor: 2, patch: 3, + prerelease: nil, buildMetadata: nil + ) + XCTAssertEqual(version2.major, 1) + XCTAssertEqual(version2.minor, 2) + XCTAssertEqual(version2.patch, 3) + XCTAssertEqual(version2.prereleaseIdentifiers, []) + XCTAssertEqual(version2.buildMetadataIdentifiers, []) + + let version3 = try SymbolGraph.SemanticVersion( + major: 42, minor: 41, patch: 40, + prerelease: "beta.1337.0", buildMetadata: nil + ) + XCTAssertEqual(version3.major, 42) + XCTAssertEqual(version3.minor, 41) + XCTAssertEqual(version3.patch, 40) + XCTAssertEqual(version3.prereleaseIdentifiers, ["beta", "1337", "0"]) + XCTAssertEqual(version3.buildMetadataIdentifiers, []) + + let version4 = try SymbolGraph.SemanticVersion( + major: 2, minor: 3, patch: 5, + prerelease: nil, buildMetadata: "2022-05-23" + ) + XCTAssertEqual(version4.major, 2) + XCTAssertEqual(version4.minor, 3) + XCTAssertEqual(version4.patch, 5) + XCTAssertEqual(version4.prereleaseIdentifiers, []) + XCTAssertEqual(version4.buildMetadataIdentifiers, ["2022-05-23"]) + + let version5 = try SymbolGraph.SemanticVersion( + major: 7, minor: 11, patch: 13, + prerelease: "alpha-1.-42", buildMetadata: "010203.md5-d41d8cd98f00b204e9800998ecf8427e" + ) + XCTAssertEqual(version5.major, 7) + XCTAssertEqual(version5.minor, 11) + XCTAssertEqual(version5.patch, 13) + XCTAssertEqual(version5.prereleaseIdentifiers, ["alpha-1", "-42"]) + XCTAssertEqual(version5.buildMetadataIdentifiers, ["010203", "md5-d41d8cd98f00b204e9800998ecf8427e"]) + + // MARK: default parameters + + let version6 = try SymbolGraph.SemanticVersion(major: 0, minor: 0, patch: 0) + XCTAssertEqual(version6.major, version1.major) + XCTAssertEqual(version6.minor, version1.minor) + XCTAssertEqual(version6.patch, version1.patch) + XCTAssertEqual(version6.prereleaseIdentifiers, version1.prereleaseIdentifiers) + XCTAssertEqual(version6.buildMetadataIdentifiers, version1.buildMetadataIdentifiers) + + let version7 = try SymbolGraph.SemanticVersion(major: 1, minor: 2, patch: 3) + XCTAssertEqual(version7.major, version2.major) + XCTAssertEqual(version7.minor, version2.minor) + XCTAssertEqual(version7.patch, version2.patch) + XCTAssertEqual(version7.prereleaseIdentifiers, version2.prereleaseIdentifiers) + XCTAssertEqual(version7.buildMetadataIdentifiers, version2.buildMetadataIdentifiers) + + let version8 = try SymbolGraph.SemanticVersion(major: 42, minor: 41, patch: 40, prerelease: "beta.1337.0") + XCTAssertEqual(version8.major, version3.major) + XCTAssertEqual(version8.minor, version3.minor) + XCTAssertEqual(version8.patch, version3.patch) + XCTAssertEqual(version8.prereleaseIdentifiers, version3.prereleaseIdentifiers) + XCTAssertEqual(version8.buildMetadataIdentifiers, version3.buildMetadataIdentifiers) + + let version9 = try SymbolGraph.SemanticVersion(major: 2, minor: 3, patch: 5, buildMetadata: "2022-05-23") + XCTAssertEqual(version9.major, version4.major) + XCTAssertEqual(version9.minor, version4.minor) + XCTAssertEqual(version9.patch, version4.patch) + XCTAssertEqual(version9.prereleaseIdentifiers, version4.prereleaseIdentifiers) + XCTAssertEqual(version9.buildMetadataIdentifiers, version4.buildMetadataIdentifiers) + + // MARK: invalid components + + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 9, minor: 8, patch: 7, prerelease: "")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 6, minor: 5, patch: 4, prerelease: " ")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 3, minor: 2, patch: 1, prerelease: "+")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 0, minor: 9, patch: 8, prerelease: "...")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 4, minor: 3, patch: 2, prerelease: ".c.")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 1, minor: 0, patch: 9, prerelease: "测试版")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 8, minor: 7, patch: 6, prerelease: "00")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 5, minor: 4, patch: 3, prerelease: "0123")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 0, minor: 1, patch: 2, buildMetadata: "")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 3, minor: 4, patch: 5, buildMetadata: " ")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 6, minor: 7, patch: 8, buildMetadata: "+")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 9, minor: 0, patch: 1, buildMetadata: "...")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 5, minor: 6, patch: 7, buildMetadata: ".c.")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 8, minor: 9, patch: 0, buildMetadata: "🙃")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } + } +} diff --git a/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/PrecedenceTests.swift b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/PrecedenceTests.swift new file mode 100644 index 0000000..2308e9f --- /dev/null +++ b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/PrecedenceTests.swift @@ -0,0 +1,254 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +import XCTest +@testable import SymbolKit + +final class PrecedenceTests: XCTestCase { + func assert(_ version1: SymbolGraph.SemanticVersion, precedes version2: SymbolGraph.SemanticVersion) { + XCTAssertLessThan(version1, version2) + XCTAssertLessThanOrEqual(version1, version2) + XCTAssertGreaterThan(version2, version1) + XCTAssertGreaterThanOrEqual(version2, version1) + XCTAssertNotEqual(version1, version2) + XCTAssertNotEqual(version2, version1) + XCTAssertFalse(version1 > version2) + XCTAssertFalse(version1 >= version2) + XCTAssertFalse(version2 < version1) + XCTAssertFalse(version2 <= version1) + } + + func assert(_ version1: SymbolGraph.SemanticVersion, equals version2: SymbolGraph.SemanticVersion) { + XCTAssertEqual(version1, version2) + XCTAssertEqual(version2, version1) + XCTAssertLessThanOrEqual(version1, version2) + XCTAssertLessThanOrEqual(version1, version2) + XCTAssertGreaterThanOrEqual(version1, version2) + XCTAssertGreaterThanOrEqual(version2, version1) + XCTAssertFalse(version1 != version2) + XCTAssertFalse(version2 != version1) + XCTAssertFalse(version1 < version2) + XCTAssertFalse(version2 < version1) + XCTAssertFalse(version1 > version2) + XCTAssertFalse(version2 > version1) + } + + func testVersionCorePrecedence() throws { + assert(try .init(major: 0, minor: 0, patch: 0), precedes: try .init(major: 0, minor: 0, patch: 1)) + assert(try .init(major: 0, minor: 0, patch: 0), precedes: try .init(major: 0, minor: 1, patch: 0)) + assert(try .init(major: 0, minor: 0, patch: 0), precedes: try .init(major: 1, minor: 0, patch: 0)) + assert(try .init(major: 1, minor: 2, patch: 3), precedes: try .init(major: 1, minor: 2, patch: 4)) + assert(try .init(major: 1, minor: 2, patch: 3), precedes: try .init(major: 1, minor: 3, patch: 3)) + assert(try .init(major: 1, minor: 2, patch: 3), precedes: try .init(major: 2, minor: 2, patch: 3)) + + assert(try .init(major: 0, minor: 0, patch: 0), equals: try .init(major: 0, minor: 0, patch: 0)) + assert(try .init(major: 1, minor: 2, patch: 3), equals: try .init(major: 1, minor: 2, patch: 3)) + assert(try .init(major: 3, minor: 2, patch: 1), equals: try .init(major: 3, minor: 2, patch: 1)) + } + + func testPrereleasePrecedence() throws { + // Test cases include different combinations of numeric and alpha-numeric identifiers, with different precedences, and try to capture edge cases. + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 4)) + assert(try .init(major: 1, minor: 2, patch: 2), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta")) + assert(try .init(major: 1, minor: 2, patch: 3), precedes: try .init(major: 1, minor: 2, patch: 4, prerelease: "beta")) + assert(try .init(major: 1, minor: 2, patch: 3), precedes: try .init(major: 1, minor: 3, patch: 3, prerelease: "beta")) + assert(try .init(major: 1, minor: 2, patch: 3), precedes: try .init(major: 2, minor: 2, patch: 3, prerelease: "beta")) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 4, prerelease: "beta")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha"), precedes: try .init(major: 1, minor: 2, patch: 4, prerelease: "beta")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 4, prerelease: "alpha")) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "betax")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta-x")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta1")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta-1")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta1x")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta-1x")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "betax1")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta-x1")) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta.x")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta.1")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta.1x")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta.x1")) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "a"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha42"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha-42"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha.42"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta")) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "1bcd"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abcd")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abcd"), precedes: try .init(major: 4, minor: 5, patch: 7, prerelease: "1bcd")) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "456"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "456.alpha"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha")) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "987")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "124")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123.456.789"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "123.456.987")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123.456.789"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "321.111.111")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "9.9.9.9.9.9"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "10")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "9.9.9.9.9.9"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "10.0.0.0.0")) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.123"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.789")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.789"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.ghi.123")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123.def-ghi"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "789.abc.def")) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "987"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "123a")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "987654321"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "0a")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "999999999"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "0a")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "999999999.zzz.zz"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "0a")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.123"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.ghi")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.987.ghi"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.123def.123")) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc-123def-123"), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc-987-ghi")) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "0a"), precedes: try .init(major: 4, minor: 5, patch: 7, prerelease: "987")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "0a"), precedes: try .init(major: 4, minor: 6, patch: 6, prerelease: "987")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "0a"), precedes: try .init(major: 5, minor: 5, patch: 6, prerelease: "987")) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta"), equals: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123abc"), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "123abc")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123-abc"), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "123-abc")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123.abc"), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "123.abc")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc123"), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc123")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc-123"), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc-123")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.123"), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.123")) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc123.123abc.123-abc.abc-123"), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc123.123abc.123-abc.abc-123")) + } + + func testBuildMetadataPrecedence() throws { + + // In addition to hardcoded different combinations of numeric and alpha-numeric identifiers, with different precedences, some build metadata is randomly generated at each run of the test. + + var randomBuildMetadata: String { + let lexicon = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-." + let length = UInt8.random(in: 1...(.max)) // don't be too long + var characters: [Character] = [] + characters.reserveCapacity(Int(length)) + for position in 1...length { + if let lastCharacter = characters.last, lastCharacter != ".", position != length { + characters.append(lexicon.randomElement()!) + } else { + characters.append(lexicon.dropLast().randomElement()!) + } + } + return String(characters) + } + + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "abc"), precedes: try .init(major: 0, minor: 0, patch: 1, buildMetadata: "abc")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "abc"), precedes: try .init(major: 0, minor: 0, patch: 1, buildMetadata: "bcd")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "bcd"), precedes: try .init(major: 0, minor: 0, patch: 1, buildMetadata: "abc")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "123"), precedes: try .init(major: 0, minor: 1, patch: 0, buildMetadata: "123")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "123"), precedes: try .init(major: 0, minor: 1, patch: 0, buildMetadata: "234")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "234"), precedes: try .init(major: 0, minor: 1, patch: 0, buildMetadata: "123")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "1a2b3c"), precedes: try .init(major: 1, minor: 0, patch: 0, buildMetadata: "1a2b3c")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "1a2b3c"), precedes: try .init(major: 1, minor: 0, patch: 0, buildMetadata: "3c2b1a")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "3c2b1a"), precedes: try .init(major: 1, minor: 0, patch: 0, buildMetadata: "1a2b3c")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "a1b2c3"), precedes: try .init(major: 1, minor: 2, patch: 4, buildMetadata: "a1b2c3")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "a1b2c3"), precedes: try .init(major: 1, minor: 2, patch: 4, buildMetadata: "c3b2a1")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "c3b2a1"), precedes: try .init(major: 1, minor: 2, patch: 4, buildMetadata: "a1b2c3")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "1-2-3"), precedes: try .init(major: 1, minor: 3, patch: 3, buildMetadata: "1-2-3")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "1-2-3"), precedes: try .init(major: 1, minor: 3, patch: 3, buildMetadata: "3-2-1")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "3-2-1"), precedes: try .init(major: 1, minor: 3, patch: 3, buildMetadata: "1-2-3")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "1.2.3"), precedes: try .init(major: 2, minor: 2, patch: 3, buildMetadata: "1.2.3")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "1.2.3"), precedes: try .init(major: 2, minor: 2, patch: 3, buildMetadata: "2.3.4")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "2.3.4"), precedes: try .init(major: 2, minor: 2, patch: 3, buildMetadata: "1.2.3")) + + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "a-b-c"), equals: try .init(major: 0, minor: 0, patch: 0, buildMetadata: "a-b-c")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "a-b-c"), equals: try .init(major: 0, minor: 0, patch: 0, buildMetadata: "c-b-a")) + assert(try .init(major: 0, minor: 0, patch: 0, buildMetadata: "c-b-a"), equals: try .init(major: 0, minor: 0, patch: 0, buildMetadata: "a-b-c")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "a.b.c"), equals: try .init(major: 1, minor: 2, patch: 3, buildMetadata: "a.b.c")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "a.b.c"), equals: try .init(major: 1, minor: 2, patch: 3, buildMetadata: "c.b.a")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: "c.b.a"), equals: try .init(major: 1, minor: 2, patch: 3, buildMetadata: "a.b.c")) + assert(try .init(major: 3, minor: 2, patch: 1, buildMetadata: "a-1.b-2.c-3"), equals: try .init(major: 3, minor: 2, patch: 1, buildMetadata: "a-1.b-2.c-3")) + assert(try .init(major: 3, minor: 2, patch: 1, buildMetadata: "a-1.b-2.c-3"), equals: try .init(major: 3, minor: 2, patch: 1, buildMetadata: "3.c-2.b-1.a")) + assert(try .init(major: 3, minor: 2, patch: 1, buildMetadata: "3.c-2.b-1.a"), equals: try .init(major: 3, minor: 2, patch: 1, buildMetadata: "a-1.b-2.c-3")) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "---"), precedes: try .init(major: 1, minor: 2, patch: 3, buildMetadata: "---")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "-"), precedes: try .init(major: 1, minor: 2, patch: 3, buildMetadata: "---")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "---"), precedes: try .init(major: 1, minor: 2, patch: 3, buildMetadata: "-")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "-.-"), precedes: try .init(major: 1, minor: 2, patch: 4, buildMetadata: "-.-")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "-.-.-"), precedes: try .init(major: 1, minor: 2, patch: 4, buildMetadata: "-.-")) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "-.-"), precedes: try .init(major: 1, minor: 2, patch: 4, buildMetadata: "-.-.-")) + assert(try .init(major: 1, minor: 2, patch: 2, buildMetadata: "000"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "000")) + assert(try .init(major: 1, minor: 2, patch: 2, buildMetadata: "000"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "0")) + assert(try .init(major: 1, minor: 2, patch: 2, buildMetadata: "0"), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "000")) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 4, prerelease: "beta", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 3, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, buildMetadata: randomBuildMetadata), precedes: try .init(major: 2, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 4, prerelease: "beta", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 4, prerelease: "beta", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 4, prerelease: "alpha", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "betax", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta-x", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta1", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta-1", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta1x", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta-1x", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "betax1", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta-x1", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta.x", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta.1", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta.1x", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta.x1", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "a", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha42", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha-42", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha.42", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "1bcd", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abcd", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abcd", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 7, prerelease: "1bcd", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "456", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "456.alpha", buildMetadata: randomBuildMetadata), precedes: try .init(major: 1, minor: 2, patch: 3, prerelease: "alpha", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "987", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "124", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123.456.789", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "123.456.987", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123.456.789", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "321.111.111", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "9.9.9.9.9.9", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "10", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "9.9.9.9.9.9", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "10.0.0.0.0", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.123", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.789", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.789", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.ghi.123", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123.def-ghi", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "789.abc.def", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "987", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "123a", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "987654321", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "0a", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "999999999", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "0a", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "999999999.zzzzz.zzzzz", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "0a", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.123", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.def.ghi", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.987.ghi", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.123def.123", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc-123def-123", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc-987-ghi", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "0a", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 5, patch: 7, prerelease: "987", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "0a", buildMetadata: randomBuildMetadata), precedes: try .init(major: 4, minor: 6, patch: 6, prerelease: "987", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "0a", buildMetadata: randomBuildMetadata), precedes: try .init(major: 5, minor: 5, patch: 6, prerelease: "987", buildMetadata: randomBuildMetadata)) + + assert(try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata), equals: try .init(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123abc", buildMetadata: randomBuildMetadata), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "123abc", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123-abc", buildMetadata: randomBuildMetadata), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "123-abc", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "123.abc", buildMetadata: randomBuildMetadata), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "123.abc", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc123", buildMetadata: randomBuildMetadata), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc123", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc-123", buildMetadata: randomBuildMetadata), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc-123", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.123", buildMetadata: randomBuildMetadata), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc.123", buildMetadata: randomBuildMetadata)) + assert(try .init(major: 4, minor: 5, patch: 6, prerelease: "abc123.123abc.123-abc.abc-123", buildMetadata: randomBuildMetadata), equals: try .init(major: 4, minor: 5, patch: 6, prerelease: "abc123.123abc.123-abc.abc-123", buildMetadata: randomBuildMetadata)) + } +} diff --git a/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/SemanticsTests.swift b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/SemanticsTests.swift new file mode 100644 index 0000000..23a80da --- /dev/null +++ b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/SemanticsTests.swift @@ -0,0 +1,137 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +import XCTest +@testable import SymbolKit + +final class SemanticsTests: XCTestCase { + func testDenotingStableRelease() throws { + let stableVersions: [SymbolGraph.SemanticVersion] = [ + try .init(major: 1, minor: 0, patch: 0), + try .init(major: 1, minor: 0, patch: 1), + try .init(major: 1, minor: 2, patch: 3), + try .init(major: 987, minor: 654, patch: 321), + try .init(major: 3, minor: 2, patch: 1, buildMetadata: "abcde") + ] + + for stableVersion in stableVersions { + XCTAssertTrue(stableVersion.denotesStableRelease) + } + + let unstableVersions: [SymbolGraph.SemanticVersion] = [ + try .init(major: 0, minor: 0, patch: 0), + try .init(major: 0, minor: 0, patch: 1), + try .init(major: 0, minor: 1, patch: 2), + try .init(major: 0, minor: 9, patch: 8, prerelease: "gm"), + try .init(major: 1, minor: 0, patch: 0, prerelease: "beta"), + try .init(major: 9, minor: 9, patch: 9, prerelease: "alpha", buildMetadata: "xyz") + ] + + for unstableVersion in unstableVersions { + XCTAssertFalse(unstableVersion.denotesStableRelease) + } + } + + func testDenotingPrerelease() throws { + let prereleaseVersions: [SymbolGraph.SemanticVersion] = [ + try .init(major: 0, minor: 0, patch: 0, prerelease: "asd"), + try .init(major: 0, minor: 0, patch: 1, prerelease: "fgh"), + try .init(major: 0, minor: 1, patch: 0, prerelease: "jkl"), + try .init(major: 0, minor: 9, patch: 8, prerelease: "qwe", buildMetadata: "rty"), + try .init(major: 1, minor: 0, patch: 0, prerelease: "uio"), + try .init(major: 1, minor: 2, patch: 3, prerelease: "zxc") + ] + + for prereleaseVersion in prereleaseVersions { + XCTAssertTrue(prereleaseVersion.denotesPrerelease) + } + + let releaseVersions: [SymbolGraph.SemanticVersion] = [ + try .init(major: 0, minor: 0, patch: 0), + try .init(major: 0, minor: 0, patch: 1), + try .init(major: 0, minor: 1, patch: 0), + try .init(major: 0, minor: 9, patch: 8, buildMetadata: "rty"), + try .init(major: 1, minor: 0, patch: 0), + try .init(major: 1, minor: 2, patch: 3), + try .init(major: 0, minor: 0, patch: 0, buildMetadata: "-asd"), + try .init(major: 0, minor: 0, patch: 1, buildMetadata: "-fgh"), + try .init(major: 0, minor: 1, patch: 0, buildMetadata: "-jkl"), + try .init(major: 0, minor: 9, patch: 8, buildMetadata: "-qwe-rty"), + try .init(major: 1, minor: 0, patch: 0, buildMetadata: "-uio"), + try .init(major: 1, minor: 2, patch: 3, buildMetadata: "-zxc") + ] + + for releaseVersion in releaseVersions { + XCTAssertFalse(releaseVersion.denotesPrerelease) + } + } + + func testDenotingSourceBreakableRelease() throws { + let sortedBreakableVersions: [SymbolGraph.SemanticVersion] = [ + try .init(major: 0, minor: 0, patch: 0, prerelease: "123"), + try .init(major: 0, minor: 0, patch: 0, prerelease: "abc"), + try .init(major: 0, minor: 0, patch: 0), + try .init(major: 0, minor: 0, patch: 1, prerelease: "456"), + try .init(major: 0, minor: 0, patch: 1, prerelease: "def"), + try .init(major: 0, minor: 0, patch: 1), + try .init(major: 0, minor: 0, patch: 2, prerelease: "789"), + try .init(major: 0, minor: 0, patch: 2, prerelease: "ghi"), + try .init(major: 0, minor: 0, patch: 2), + try .init(major: 0, minor: 1, patch: 0, prerelease: "876"), + try .init(major: 0, minor: 1, patch: 0, prerelease: "jkl"), + try .init(major: 0, minor: 1, patch: 0), + try .init(major: 0, minor: 1, patch: 2, prerelease: "543"), + try .init(major: 0, minor: 1, patch: 2, prerelease: "mno"), + try .init(major: 0, minor: 1, patch: 2), + try .init(major: 1, minor: 0, patch: 0, prerelease: "212"), + try .init(major: 1, minor: 0, patch: 0, prerelease: "pqr"), + try .init(major: 1, minor: 0, patch: 0), + try .init(major: 2, minor: 0, patch: 0, prerelease: "345"), + try .init(major: 2, minor: 0, patch: 0, prerelease: "stu"), + try .init(major: 2, minor: 0, patch: 0), + try .init(major: 3, minor: 4, patch: 5, prerelease: "678"), + try .init(major: 3, minor: 4, patch: 5, prerelease: "vwx"), + try .init(major: 3, minor: 4, patch: 5) + ] + + for newVersionIndex in 1.. Date: Fri, 27 May 2022 10:59:01 +0800 Subject: [PATCH 2/9] =?UTF-8?q?revert=20the=20main=20swift=20version=20spe?= =?UTF-8?q?cifier=20back=20to=20=E2=80=9C5.2=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It appears that whatever caused the type checking problems before has already been fixed. --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index ebb0c07..6d82c3b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.6 +// swift-tools-version:5.2 // In order to support users running on the latest Xcodes, please ensure that // Package@swift-5.5.swift is kept in sync with this file. /* From 4e2b3093a5717c8cd14212a266f4e6e47510a138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=80=E5=8D=93=E7=96=8C?= <55120045+WowbaggersLiquidLunch@users.noreply.github.com> Date: Fri, 27 May 2022 11:10:22 +0800 Subject: [PATCH 3/9] elaborate a bit more on why we cannot omit empty subsequences when splitting the prerelease into identifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Rönnqvist --- Sources/SymbolKit/SymbolGraph/SemanticVersion/Prerelease.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/Prerelease.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/Prerelease.swift index e7208ff..3b55e91 100644 --- a/Sources/SymbolKit/SymbolGraph/SemanticVersion/Prerelease.swift +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/Prerelease.swift @@ -42,7 +42,7 @@ extension SymbolGraph.SemanticVersion.Prerelease { } let identifiers = dotSeparatedIdentifiers.split( separator: ".", - omittingEmptySubsequences: false // must preserve empty identifiers, for accurate diagnostics. + omittingEmptySubsequences: false // Preserve empty sequences to be able to raise validation errors about empty prerelease identifiers. ) try self.init(identifiers) } From b5f7003407ce6842a116f97c8edb55523296271c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=80=E5=8D=93=E7=96=8C?= <55120045+WowbaggersLiquidLunch@users.noreply.github.com> Date: Fri, 27 May 2022 11:19:39 +0800 Subject: [PATCH 4/9] use the convenient computed property `isAllowedInSemanticVersionIdentifier` when validating build metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Rönnqvist --- .../SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion.swift index 5d3ad4e..6ac13b6 100644 --- a/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion.swift +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion.swift @@ -51,7 +51,7 @@ extension SymbolGraph { throw SymbolGraph.SemanticVersionError.emptyIdentifier(position: .buildMetadata) } try buildMetadataIdentifiers.forEach { - guard $0.allSatisfy( { $0.isASCII && ( $0.isLetter || $0.isNumber || $0 == "-" ) } ) else { + guard $0.allSatisfy(\.isAllowedInSemanticVersionIdentifier) else { throw SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier($0, position: .buildMetadata) } } From ecbede76bd37700d5d32b2b5e55ae54399abe7bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=80=E5=8D=93=E7=96=8C?= <55120045+WowbaggersLiquidLunch@users.noreply.github.com> Date: Fri, 27 May 2022 11:39:14 +0800 Subject: [PATCH 5/9] defer assignment to `position`-prefixed constants to inside the switch statements --- .../SemanticVersionError.swift | 16 +- .../SemanticVersion/ErrorTests.swift | 146 ++++++++---------- 2 files changed, 74 insertions(+), 88 deletions(-) diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersionError.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersionError.swift index 82ebcd3..17ea67e 100644 --- a/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersionError.swift +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersionError.swift @@ -97,16 +97,12 @@ extension SymbolGraph { case let .invalidCharacterInIdentifier(identifier, position): return "semantic version \(position) identifier '\(identifier)' cannot contain characters other than ASCII alphanumerics and hyphen-minus ([0-9A-Za-z-])" case let .invalidNumericIdentifier(identifier, position, errorKind): - let fault: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 - switch errorKind { - case .leadingZeros: - return "contain leading '0'" - case .nonNumericCharacter: - return "contain non-numeric characters" - case .oversizedValue: - return "be larger than 'UInt.max'" - } - }() + let fault: String + switch errorKind { + case .leadingZeros: fault = "contain leading '0'" + case .nonNumericCharacter: fault = "contain non-numeric characters" + case .oversizedValue: fault = "be larger than 'UInt.max'" + } return "semantic version \(position) identifier '\(identifier)' cannot \(fault)" case let .invalidVersionCoreIdentifierCount(identifiers): return """ diff --git a/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/ErrorTests.swift b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/ErrorTests.swift index b454129..6660554 100644 --- a/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/ErrorTests.swift +++ b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/ErrorTests.swift @@ -201,15 +201,14 @@ final class ErrorTests: XCTestCase { atPosition position: SymbolGraph.SemanticVersionError.IdentifierPosition, whenEvaluating expression: @autoclosure () throws -> SymbolGraph.SemanticVersion ) { - let positionString: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 - switch position { - case .major: return "major" - case .minor: return "minor" - case .patch: return "patch" - case .prerelease: return "prerelease" - case .buildMetadata: return "buildMetadata" - } - }() + let positionString: String + switch position { + case .major: positionString = "major" + case .minor: positionString = "minor" + case .patch: positionString = "patch" + case .prerelease: positionString = "prerelease" + case .buildMetadata: positionString = "buildMetadata" + } XCTAssertThrowsError( try expression(), "'SymbolGraph.SemanticVersionError.emptyIdentifier(position: .\(positionString))' should've been thrown, but no error is thrown" @@ -218,15 +217,14 @@ final class ErrorTests: XCTestCase { XCTFail(#"'SymbolGraph.SemanticVersionError.emptyIdentifier(position: .\#(positionString))' should've been thrown, but a different error is thrown instead; error description: "\#(error)""#) return } - let positionDescription: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 - switch position { - case .major: return "major version number" - case .minor: return "minor version number" - case .patch: return "patch version number" - case .prerelease: return "pre-release" - case .buildMetadata: return "build metadata" - } - }() + let positionDescription: String + switch position { + case .major: positionDescription = "major version number" + case .minor: positionDescription = "minor version number" + case .patch: positionDescription = "patch version number" + case .prerelease: positionDescription = "pre-release" + case .buildMetadata: positionDescription = "build metadata" + } XCTAssertEqual( error.description, "semantic version \(positionDescription) identifier cannot be empty" @@ -239,12 +237,11 @@ final class ErrorTests: XCTestCase { inIdentifier identifier: Substring, whenEvaluating expression: @autoclosure () throws -> SymbolGraph.SemanticVersion ) { - let positionString: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 - switch position { - case .prerelease: return "prerelease" - case .buildMetadata: return "buildMetadata" - } - }() + let positionString: String + switch position { + case .prerelease: positionString = "prerelease" + case .buildMetadata: positionString = "buildMetadata" + } XCTAssertThrowsError( try expression(), "'SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(\(identifier), position: .\(positionString))' should've been thrown, but no error is thrown" @@ -253,12 +250,11 @@ final class ErrorTests: XCTestCase { XCTFail((#"'SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(\#(identifier), position: .\#(positionString))' should've been thrown, but a different error is thrown instead; error description: "\#(error)""#)) return } - let positionDescription: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 - switch position { - case .prerelease: return "pre-release" - case .buildMetadata: return "build metadata" - } - }() + let positionDescription: String + switch position { + case .prerelease: positionDescription = "pre-release" + case .buildMetadata: positionDescription = "build metadata" + } XCTAssertEqual( error.description, "semantic version \(positionDescription) identifier '\(identifier)' cannot contain characters other than ASCII alphanumerics and hyphen-minus ([0-9A-Za-z-])" @@ -271,14 +267,13 @@ final class ErrorTests: XCTestCase { inIdentifier identifier: Substring, whenEvaluating expression: @autoclosure () throws -> SymbolGraph.SemanticVersion ) { - let positionString: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 - switch position { - case .major: return "major" - case .minor: return "minor" - case .patch: return "patch" - case .prerelease: XCTFail("pre-release identifier with non-numeric characters should be regarded as alpha-numeric identifier"); return "prerelease" - } - }() + let positionString: String + switch position { + case .major: positionString = "major" + case .minor: positionString = "minor" + case .patch: positionString = "patch" + case .prerelease: XCTFail("pre-release identifier with non-numeric characters should be regarded as alpha-numeric identifier"); positionString = "prerelease" + } XCTAssertThrowsError( try expression(), "'SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(\(identifier), position: .\(positionString))' should've been thrown, but no error is thrown" @@ -287,14 +282,13 @@ final class ErrorTests: XCTestCase { XCTFail((#"'SymbolGraph.SemanticVersionError.invalidNumericIdentifier(\#(identifier), position: \#(positionString), errorKind: .nonNumericCharacter)' should've been thrown, but a different error is thrown instead; error description: "\#(error)""#)) return } - let positionDescription: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 - switch position { - case .major: return "major version number" - case .minor: return "minor version number" - case .patch: return "patch version number" - case .prerelease: XCTFail("pre-release identifier with non-numeric characters should be regarded as alpha-numeric identifier"); return "pre-release numeric" - } - }() + let positionDescription: String + switch position { + case .major: positionDescription = "major version number" + case .minor: positionDescription = "minor version number" + case .patch: positionDescription = "patch version number" + case .prerelease: XCTFail("pre-release identifier with non-numeric characters should be regarded as alpha-numeric identifier"); positionDescription = "pre-release numeric" + } XCTAssertEqual( error.description, "semantic version \(positionDescription) identifier '\(identifier)' cannot contain non-numeric characters" @@ -307,14 +301,13 @@ final class ErrorTests: XCTestCase { inIdentifier identifier: Substring, whenEvaluating expression: @autoclosure () throws -> SymbolGraph.SemanticVersion ) { - let positionString: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 - switch position { - case .major: return "major" - case .minor: return "minor" - case .patch: return "patch" - case .prerelease: return "prerelease" - } - }() + let positionString: String + switch position { + case .major: positionString = "major" + case .minor: positionString = "minor" + case .patch: positionString = "patch" + case .prerelease: positionString = "prerelease" + } XCTAssertThrowsError( try expression(), "'SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(\(identifier), position: .\(positionString))' should've been thrown, but no error is thrown" @@ -323,14 +316,13 @@ final class ErrorTests: XCTestCase { XCTFail((#"'SymbolGraph.SemanticVersionError.invalidNumericIdentifier(\#(identifier), position: \#(positionString), errorKind: .leadingZeros)' should've been thrown, but a different error is thrown instead; error description: "\#(error)""#)) return } - let positionDescription: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 - switch position { - case .major: return "major version number" - case .minor: return "minor version number" - case .patch: return "patch version number" - case .prerelease: return "pre-release numeric" - } - }() + let positionDescription: String + switch position { + case .major: positionDescription = "major version number" + case .minor: positionDescription = "minor version number" + case .patch: positionDescription = "patch version number" + case .prerelease: positionDescription = "pre-release numeric" + } XCTAssertEqual( error.description, "semantic version \(positionDescription) identifier '\(identifier)' cannot contain leading '0'" @@ -343,14 +335,13 @@ final class ErrorTests: XCTestCase { inIdentifier identifier: Substring, whenEvaluating expression: @autoclosure () throws -> SymbolGraph.SemanticVersion ) { - let positionString: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 - switch position { - case .major: return "major" - case .minor: return "minor" - case .patch: return "patch" - case .prerelease: return "prerelease" - } - }() + let positionString: String + switch position { + case .major: positionString = "major" + case .minor: positionString = "minor" + case .patch: positionString = "patch" + case .prerelease: positionString = "prerelease" + } XCTAssertThrowsError( try expression(), "'SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(\(identifier), position: .\(positionString))' should've been thrown, but no error is thrown" @@ -359,14 +350,13 @@ final class ErrorTests: XCTestCase { XCTFail((#"'SymbolGraph.SemanticVersionError.invalidNumericIdentifier(\#(identifier), position: \#(positionString), errorKind: .oversizedValue)' should've been thrown, but a different error is thrown instead; error description: "\#(error)""#)) return } - let positionDescription: String = { // FIXME: This type annotation can be removed in Swift ≥ 5.7 - switch position { - case .major: return "major version number" - case .minor: return "minor version number" - case .patch: return "patch version number" - case .prerelease: return "pre-release numeric" - } - }() + let positionDescription: String + switch position { + case .major: positionDescription = "major version number" + case .minor: positionDescription = "minor version number" + case .patch: positionDescription = "patch version number" + case .prerelease: positionDescription = "pre-release numeric" + } XCTAssertEqual( error.description, "semantic version \(positionDescription) identifier '\(identifier)' cannot be larger than 'UInt.max'" From 1790ae6e0d7548acdfdfefe3c7f11d32c90c1862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=80=E5=8D=93=E7=96=8C?= <55120045+WowbaggersLiquidLunch@users.noreply.github.com> Date: Fri, 27 May 2022 17:16:39 +0800 Subject: [PATCH 6/9] move common identifier validation logic out of `Prerelease.swift` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s used for more than validating prerelease. --- .../Identifier Validation.swift | 21 +++++++++++++++++++ .../SemanticVersion/Prerelease.swift | 14 ------------- 2 files changed, 21 insertions(+), 14 deletions(-) create mode 100644 Sources/SymbolKit/SymbolGraph/SemanticVersion/Identifier Validation.swift diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/Identifier Validation.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/Identifier Validation.swift new file mode 100644 index 0000000..b286e66 --- /dev/null +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/Identifier Validation.swift @@ -0,0 +1,21 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +extension Character { + /// A Boolean value indicating whether this character is allowed in a semantic version's identifier. + internal var isAllowedInSemanticVersionIdentifier: Bool { + isASCII && (isLetter || isNumber || self == "-") + } + + /// A Boolean value indicating whether this character is allowed in a semantic version's numeric identifier. + internal var isAllowedInSemanticVersionNumericIdentifier: Bool { + isASCII && isNumber + } +} diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/Prerelease.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/Prerelease.swift index 3b55e91..863ab5a 100644 --- a/Sources/SymbolKit/SymbolGraph/SemanticVersion/Prerelease.swift +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/Prerelease.swift @@ -142,17 +142,3 @@ extension SymbolGraph.SemanticVersion.Prerelease.Identifier: CustomStringConvert } } } - -// MARK: - Validating Identifiers - -extension Character { - /// A Boolean value indicating whether this character is allowed in a semantic version's identifier. - internal var isAllowedInSemanticVersionIdentifier: Bool { - isASCII && (isLetter || isNumber || self == "-") - } - - /// A Boolean value indicating whether this character is allowed in a semantic version's numeric identifier. - internal var isAllowedInSemanticVersionNumericIdentifier: Bool { - isASCII && isNumber - } -} From 4d9783d7a4497158fbef6778cc04b65e82f70897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=80=E5=8D=93=E7=96=8C?= <55120045+WowbaggersLiquidLunch@users.noreply.github.com> Date: Fri, 27 May 2022 17:40:55 +0800 Subject: [PATCH 7/9] add a non-throwing initializer that takes only version core identifiers This initializer should be much more ergonomic to use in the common cases, and it's safe because it's guaranteed not to encounter any error. --- .../SemanticVersion/SemanticVersion.swift | 16 +++++++++++++++ ....swift => DirectInitializationTests.swift} | 20 +++++++++++++++++-- .../SymbolGraphCreationTests.swift | 4 ++-- 3 files changed, 36 insertions(+), 4 deletions(-) rename Tests/SymbolKitTests/SymbolGraph/SemanticVersion/{MainInitializerTests.swift => DirectInitializationTests.swift} (89%) diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion.swift index 6ac13b6..339d8e0 100644 --- a/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion.swift +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion.swift @@ -58,6 +58,13 @@ extension SymbolGraph { self.buildMetadataIdentifiers = buildMetadataIdentifiers } + /// Creates a semantic version with the provided components of a semantic version. + /// - Parameters: + /// - major: The major version number. + /// - minor: The minor version number. + /// - patch: The patch version number. + /// - prerelease: The pre-release information. + /// - buildMetadata: The build metadata. @available(*, deprecated, renamed: "init(_:_:_:prerelease:buildMetadata:)") @_disfavoredOverload public init( @@ -75,6 +82,15 @@ extension SymbolGraph { buildMetadata: buildMetadata ) } + + /// Creates a semantic version with the provided components of a semantic version. + /// - Parameters: + /// - major: The major version number. + /// - minor: The minor version number. + /// - patch: The patch version number. + public init(_ major: UInt, _ minor: UInt, _ patch: UInt) { + try! self.init(major: major, minor: minor, patch: patch) + } } } diff --git a/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/MainInitializerTests.swift b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/DirectInitializationTests.swift similarity index 89% rename from Tests/SymbolKitTests/SymbolGraph/SemanticVersion/MainInitializerTests.swift rename to Tests/SymbolKitTests/SymbolGraph/SemanticVersion/DirectInitializationTests.swift index 0d151a5..f5e3fd6 100644 --- a/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/MainInitializerTests.swift +++ b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/DirectInitializationTests.swift @@ -11,8 +11,8 @@ import XCTest @testable import SymbolKit -final class MainInitializationTests: XCTestCase { - func testInitializationFromComponents() throws { +final class DirectInitializationTests: XCTestCase { + func testInitializationFromAllComponents() throws { // MARK: primary public properties @@ -114,4 +114,20 @@ final class MainInitializationTests: XCTestCase { XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 5, minor: 6, patch: 7, buildMetadata: ".c.")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } XCTAssertThrowsError(try SymbolGraph.SemanticVersion(major: 8, minor: 9, patch: 0, buildMetadata: "🙃")) { XCTAssertTrue($0 is SymbolGraph.SemanticVersionError) } } + + func testInitializationFromOnlyVersionCoreComponents() { + let version1 = SymbolGraph.SemanticVersion(0, 0, 0) + XCTAssertEqual(version1.major, 0) + XCTAssertEqual(version1.minor, 0) + XCTAssertEqual(version1.patch, 0) + XCTAssertEqual(version1.prereleaseIdentifiers, []) + XCTAssertEqual(version1.buildMetadataIdentifiers, []) + + let version2 = SymbolGraph.SemanticVersion(3, 2, 1) + XCTAssertEqual(version2.major, 3) + XCTAssertEqual(version2.minor, 2) + XCTAssertEqual(version2.patch, 1) + XCTAssertEqual(version2.prereleaseIdentifiers, []) + XCTAssertEqual(version2.buildMetadataIdentifiers, []) + } } diff --git a/Tests/SymbolKitTests/SymbolGraph/SymbolGraphCreationTests.swift b/Tests/SymbolKitTests/SymbolGraph/SymbolGraphCreationTests.swift index e6c0ea8..7484337 100644 --- a/Tests/SymbolKitTests/SymbolGraph/SymbolGraphCreationTests.swift +++ b/Tests/SymbolKitTests/SymbolGraph/SymbolGraphCreationTests.swift @@ -20,7 +20,7 @@ class SymbolGraphCreationTests: XCTestCase { func testCreateAndEncodeSymbolGraph() throws { let symbolGraph = SymbolGraph( metadata: .init( - formatVersion: try .init(major: 0, minor: 5, patch: 0), + formatVersion: .init(0, 5, 0), generator: "org.swift.SymbolKitTests" ), module: .init( @@ -30,7 +30,7 @@ class SymbolGraphCreationTests: XCTestCase { vendor: nil, operatingSystem: .init( name: "MyOS", - minimumVersion: try .init(major: 1, minor: 2, patch: 3) + minimumVersion: .init(1, 2, 3) ), environment: nil ) From c27b2bff1f52503a9748e90fda7ee77242537492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=80=E5=8D=93=E7=96=8C?= <55120045+WowbaggersLiquidLunch@users.noreply.github.com> Date: Sat, 4 Jun 2022 14:15:41 +0800 Subject: [PATCH 8/9] add a comment referring to the SR-14665 Reference to [SR-14665](https://github.com/apple/swift/issues/57016) links to more explanation on why the manual `==` implemenatation is necessary. --- .../SymbolGraph/SemanticVersion/SemanticVersion+Comparable.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion+Comparable.swift b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion+Comparable.swift index e3492ea..3febe55 100644 --- a/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion+Comparable.swift +++ b/Sources/SymbolKit/SymbolGraph/SemanticVersion/SemanticVersion+Comparable.swift @@ -10,6 +10,7 @@ extension SymbolGraph.SemanticVersion: Comparable { // Although `Comparable` inherits from `Equatable`, it does not provide a new default implementation of `==`, but instead uses `Equatable`'s default synthesised implementation. The compiler-synthesised `==`` is composed of [member-wise comparisons](https://github.com/apple/swift-evolution/blob/main/proposals/0185-synthesize-equatable-hashable.md#implementation-details), which leads to a false `false` when 2 semantic versions differ by only their build metadata identifiers, contradicting SemVer 2.0.0's [comparison rules](https://semver.org/#spec-item-10). + // [SR-14665](https://github.com/apple/swift/issues/57016) /// Returns a Boolean value indicating whether two semantic versions are equal. /// - Parameters: /// - lhs: A semantic version to compare. From 98a49de378d5fdb26b56de9d927eea42219ddf43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=80=E5=8D=93=E7=96=8C?= <55120045+WowbaggersLiquidLunch@users.noreply.github.com> Date: Sat, 4 Jun 2022 14:17:31 +0800 Subject: [PATCH 9/9] hints (a little) on why `XCAssertFalse` is necessary for precednece assertions --- .../SymbolGraph/SemanticVersion/PrecedenceTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/PrecedenceTests.swift b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/PrecedenceTests.swift index 2308e9f..2557299 100644 --- a/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/PrecedenceTests.swift +++ b/Tests/SymbolKitTests/SymbolGraph/SemanticVersion/PrecedenceTests.swift @@ -19,6 +19,7 @@ final class PrecedenceTests: XCTestCase { XCTAssertGreaterThanOrEqual(version2, version1) XCTAssertNotEqual(version1, version2) XCTAssertNotEqual(version2, version1) + // test false paths XCTAssertFalse(version1 > version2) XCTAssertFalse(version1 >= version2) XCTAssertFalse(version2 < version1) @@ -32,6 +33,7 @@ final class PrecedenceTests: XCTestCase { XCTAssertLessThanOrEqual(version1, version2) XCTAssertGreaterThanOrEqual(version1, version2) XCTAssertGreaterThanOrEqual(version2, version1) + // test false paths XCTAssertFalse(version1 != version2) XCTAssertFalse(version2 != version1) XCTAssertFalse(version1 < version2)