From b08912ce1188455ce68ed05e6a1764fe273fbc1e Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Fri, 10 Feb 2023 19:31:07 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20wildcard=20co?= =?UTF-8?q?nstraints=20(#224)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PhpVersionNumberCollection.swift | 17 +++++++ .../PHP/PHP Version/VersionNumber.swift | 36 +++++++++----- .../PHP/Switcher/InternalSwitcher.swift | 1 - .../unit/Versions/PhpVersionNumberTest.swift | 47 +++++++++++++++++++ 4 files changed, 87 insertions(+), 14 deletions(-) diff --git a/phpmon/Common/PHP/PHP Version/PhpVersionNumberCollection.swift b/phpmon/Common/PHP/PHP Version/PhpVersionNumberCollection.swift index 029e262..3a408c5 100644 --- a/phpmon/Common/PHP/PHP Version/PhpVersionNumberCollection.swift +++ b/phpmon/Common/PHP/PHP Version/PhpVersionNumberCollection.swift @@ -35,6 +35,7 @@ public struct PhpVersionNumberCollection: Equatable { - Parameter strict: Whether the patch version check is strict. See more below. The strict mode does not matter if a patch version is provided for all versions in the collection. + It also does not matter for certain comparisons (e.g. when dealing with wildcards). Strict mode assumes that any PHP version lacking precise patch information, e.g. inferred from Homebrew corresponds to the .0 patch version of that version. The default, which is imprecise, @@ -45,6 +46,7 @@ public struct PhpVersionNumberCollection: Equatable { Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in strict mode only 8.1.? will be considered valid (8.0 translates to 8.0.0 and as such is older than 8.0.1, 8.1.0 is OK). + When checking against actual PHP versions installed by the user (with patch precision), use strict mode. @@ -52,11 +54,26 @@ public struct PhpVersionNumberCollection: Equatable { Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in non-strict mode version 8.0 is assumed to be equal to version 8.0.999, which is actually fine if 8.0.1 is the required version. + In non-strict mode, the patch version is ignored for regular version checks (no caret / tilde). If checking compatibility with general Homebrew versions of PHP, do NOT use strict mode, since the patch version there is not used. (The formula php@8.0 suffices for ^8.0.1.) */ public func matching(constraint: String, strict: Bool = false) -> [VersionNumber] { + if constraint == "*" { + return self.versions + } + + if let version = VersionNumber.make(from: constraint, type: .wildCardPatch) { + // Wildcard for patch (e.g. "7.4.*") must match major and minor (any patch) + return self.versions.filter { $0.hasSameMajorAndMinor(version) } + } + + if let version = VersionNumber.make(from: constraint, type: .wildCardMinor) { + // Strict constraint (e.g. "7.*") -> must only match major (any patch, minor) + return self.versions.filter { $0.isSameMajorVersionAs(version) } + } + if let version = VersionNumber.make(from: constraint, type: .versionOnly) { // Strict constraint (e.g. "7.0") -> returns specific version return self.versions.filter { $0.isSameAs(version, strict) } diff --git a/phpmon/Common/PHP/PHP Version/VersionNumber.swift b/phpmon/Common/PHP/PHP Version/VersionNumber.swift index b01e6c0..fc400ec 100644 --- a/phpmon/Common/PHP/PHP Version/VersionNumber.swift +++ b/phpmon/Common/PHP/PHP Version/VersionNumber.swift @@ -39,6 +39,8 @@ public struct VersionNumber: Equatable, Hashable { public enum MatchType: String { case versionOnly = #"^(?\d+).(?\d+).?(?\d+)?\z"# + case wildCardPatch = #"^(?\d+).(?\d+).?(?\*)?\z"# + case wildCardMinor = #"^(?\d+).(?\*)?\z"# case caretVersionRange = #"^\^(?\d+).(?\d+).?(?\d+)?\z"# case tildeVersionRange = #"^~(?\d+).(?\d+).?(?\d+)?\z"# case greaterThanOrEqual = #"^>=(?\d+).(?\d+).?(?\d+)?\z"# @@ -64,21 +66,25 @@ public struct VersionNumber: Equatable, Hashable { range: NSRange(location: 0, length: versionString.count) ).first - if match != nil { - let major = Int( - versionString[Range(match!.range(withName: "major"), in: versionString)!] - )! - let minor = Int( - versionString[Range(match!.range(withName: "minor"), in: versionString)!] - )! - var patch: Int? - if let minorRange = Range(match!.range(withName: "patch"), in: versionString) { - patch = Int(versionString[minorRange]) - } - return Self(major: major, minor: minor, patch: patch) + guard let match else { return nil } + + let major = Int(versionString[Range(match.range(withName: "major"), in: versionString)!])! + var minor: Int = 0 + var patch: Int? + + if let minorRange = Range(match.range(withName: "minor"), in: versionString) { + let value = versionString[minorRange] as String + // Zero is the fallback if a wildcard was used + minor = Int(value) ?? 0 } - return nil + if let patchRange = Range(match.range(withName: "patch"), in: versionString) { + let value = versionString[patchRange] as String + // nil is the fallback if a wildcard was used + patch = Int(value) ?? nil + } + + return Self(major: major, minor: minor, patch: patch) } // MARK: Comparison Logic @@ -93,6 +99,10 @@ public struct VersionNumber: Equatable, Hashable { && (strict ? self.patch(strict, version) == version.patch(strict) : true) } + internal func hasSameMajorAndMinor(_ version: VersionNumber) -> Bool { + return self.major == version.major && self.minor == version.minor + } + internal func isNewerThan(_ version: VersionNumber, _ strict: Bool) -> Bool { return ( self.major > version.major || diff --git a/phpmon/Common/PHP/Switcher/InternalSwitcher.swift b/phpmon/Common/PHP/Switcher/InternalSwitcher.swift index 9526b93..5b465aa 100644 --- a/phpmon/Common/PHP/Switcher/InternalSwitcher.swift +++ b/phpmon/Common/PHP/Switcher/InternalSwitcher.swift @@ -22,7 +22,6 @@ class InternalSwitcher: PhpSwitcher { */ func performSwitch(to version: String) async { Log.info("Switching to \(version), unlinking all versions...") - let versions = getVersionsToBeHandled(version) await withTaskGroup(of: String.self, body: { group in diff --git a/tests/unit/Versions/PhpVersionNumberTest.swift b/tests/unit/Versions/PhpVersionNumberTest.swift index e2744b0..d12f3dc 100644 --- a/tests/unit/Versions/PhpVersionNumberTest.swift +++ b/tests/unit/Versions/PhpVersionNumberTest.swift @@ -44,6 +44,53 @@ class PhpVersionNumberTest: XCTestCase { } } + func test_can_parse_wildcard() throws { + let version = VersionNumber.make(from: "7.*", type: .wildCardMinor) + XCTAssertNotNil(version) + XCTAssertEqual(version!.major, 7) + XCTAssertEqual(version!.minor, 0) + } + + + func test_can_check_wildcard_version_constraint() throws { + // Wildcard for patch only + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4.10", "7.3.10", "7.3.9"]) + .matching(constraint: "7.3.*", strict: false), + PhpVersionNumberCollection + .make(from: ["7.3.10", "7.3.9"]).all + ) + + // Wildcard for minor + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["8.0.0", "7.4.10", "7.3.10", "7.3.9"]) + .matching(constraint: "7.*", strict: false), + PhpVersionNumberCollection + .make(from: ["7.4.10", "7.3.10", "7.3.9"]).all + ) + + // Full wildcard + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]) + .matching(constraint: "*", strict: false), + PhpVersionNumberCollection + .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all + ) + } + + func test_can_check_any_version_constraint() throws { + XCTAssertEqual( + PhpVersionNumberCollection + .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]) + .matching(constraint: "*", strict: false), + PhpVersionNumberCollection + .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all + ) + } + func test_can_check_fixed_constraints() throws { XCTAssertEqual( PhpVersionNumberCollection