From c2892626d7679aaee6872afe20f3b7bdc99c8c09 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Wed, 16 Oct 2024 15:13:29 -0400 Subject: [PATCH 01/20] Fix search & uninstall tests. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Tests/masTests/Commands/SearchSpec.swift | 7 +++++-- Tests/masTests/Commands/UninstallSpec.swift | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Tests/masTests/Commands/SearchSpec.swift b/Tests/masTests/Commands/SearchSpec.swift index 2496ad0..91a3a45 100644 --- a/Tests/masTests/Commands/SearchSpec.swift +++ b/Tests/masTests/Commands/SearchSpec.swift @@ -6,6 +6,7 @@ // Copyright © 2018 mas-cli. All rights reserved. // +import Foundation import Nimble import Quick @@ -31,9 +32,11 @@ public class SearchSpec: QuickSpec { ) storeSearch.apps[mockResult.trackId] = mockResult expect { - try Mas.Search.parse(["slack"]).run(storeSearch: storeSearch) + try captureStream(stdout) { + try Mas.Search.parse(["slack"]).run(storeSearch: storeSearch) + } } - .toNot(throwError()) + == " 1111 slack (0.0)\n" } it("fails when searching for nonexistent app") { expect { diff --git a/Tests/masTests/Commands/UninstallSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift index 13613b8..bf5890f 100644 --- a/Tests/masTests/Commands/UninstallSpec.swift +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -67,7 +67,7 @@ public class UninstallSpec: QuickSpec { try uninstall.run(appLibrary: mockLibrary) } } - == " 1111 slack (0.0)\n==> Some App /tmp/Some.app\n==> (not removed, dry run)\n" + == "==> Some App /tmp/Some.app\n==> (not removed, dry run)\n" } it("fails if there is a problem with the trash command") { var brokenUninstall = app // make mutable copy From 57da9e0f51d48e6c71cf5838e75fa33ef3358b51 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 20 Oct 2024 08:05:11 -0400 Subject: [PATCH 02/20] Improve `style.md`. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- docs/style.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/style.md b/docs/style.md index e4c48cc..c0487ba 100644 --- a/docs/style.md +++ b/docs/style.md @@ -5,7 +5,9 @@ - Use `script/lint` to look for these before committing. - Note that [two trailing spaces](https://gist.github.com/shaunlebron/746476e6e7a4d698b373) is intentional in markdown documents to create a line break like `
`, so these should _not_ be removed. -- End each file with a [newline character](https://unix.stackexchange.com/questions/18743/whats-the-point-in-adding-a-new-line-to-the-end-of-a-file#18789). +- End each file with a [newline character]( + https://unix.stackexchange.com/questions/18743/whats-the-point-in-adding-a-new-line-to-the-end-of-a-file#18789 + ). ## Swift @@ -14,13 +16,13 @@ - Avoid [force unwrapping optionals](https://blog.timac.org/2017/0628-swift-banning-force-unwrapping-optionals/) with `!` in production code - Production code is what gets shipped with the app. Basically, everything under the - [`mas-cli/`](https://github.com/mas-cli/mas/tree/main/mas-cli) folder. + [`Sources/mas`](https://github.com/mas-cli/mas/tree/main/Sources/mas) folder. - However, force unwrapping is **encouraged** in tests for less code and tests _should_ break when any expected conditions aren't met. - Prefer `struct`s over `class`es wherever possible - Default to marking classes as `final` - Prefer protocol conformance to class inheritance -- Break long lines after 120 characters +- Break lines at 120 characters - Use 4 spaces for indentation - Use `let` whenever possible to make immutable variables - Name all parameters in functions and enum cases @@ -28,10 +30,7 @@ with `!` in production code - Let the compiler infer the type whenever possible - Group computed properties below stored properties - Use a blank line above and below computed properties -- Group methods into specific extensions for each level of access control +- Group functions into separate extensions for each level of access control - When capitalizing acronyms or initialisms, follow the capitalization of the first letter. - When using `Void` in function signatures, prefer `()` for arguments and `Void` for return types. -- Prefer strong `IBOutlet` references. - Avoid evaluating a weak reference multiple times in the same scope. Strongify first, then use the strong reference. -- Prefer to name `IBAction` and target/action methods using a verb describing the action it will trigger, instead - of the user action (e.g., `edit:` instead of `editTapped:`) From 3d9ea972f9fcc829a5bdfa2ac5d5d3314f718246 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 20 Oct 2024 08:09:01 -0400 Subject: [PATCH 03/20] Cleanup code. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/Downloader.swift | 2 +- Sources/mas/Commands/Outdated.swift | 2 -- Sources/mas/Controllers/MasStoreSearch.swift | 8 +++++--- Sources/mas/Formatters/Utilities.swift | 2 +- Tests/masTests/Commands/UninstallSpec.swift | 2 +- Tests/masTests/Commands/VersionSpec.swift | 2 +- Tests/masTests/Controllers/AppLibraryMock.swift | 2 +- Tests/masTests/Errors/MASErrorTestCase.swift | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/mas/AppStore/Downloader.swift b/Sources/mas/AppStore/Downloader.swift index 65a2373..5f90035 100644 --- a/Sources/mas/AppStore/Downloader.swift +++ b/Sources/mas/AppStore/Downloader.swift @@ -36,7 +36,7 @@ func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise { private func downloadWithRetries(_ appID: AppID, purchase: Bool = false, attempts: Int = 3) -> Promise { SSPurchase().perform(appID: appID, purchase: purchase) - .recover { error -> Promise in + .recover { error in guard attempts > 1 else { throw error } diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift index f7b3ce5..9c0b1be 100644 --- a/Sources/mas/Commands/Outdated.swift +++ b/Sources/mas/Commands/Outdated.swift @@ -10,8 +10,6 @@ import ArgumentParser import Foundation import PromiseKit -import enum Swift.Result - extension Mas { /// Command which displays a list of installed apps which have available updates /// ready to be installed from the Mac App Store. diff --git a/Sources/mas/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/MasStoreSearch.swift index 9cf2405..6e9325a 100644 --- a/Sources/mas/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/MasStoreSearch.swift @@ -45,7 +45,7 @@ class MasStoreSearch: StoreSearch { entities += [.iPadSoftware, .iPhoneSoftware] } - let results = entities.map { entity -> Promise<[SearchResult]> in + let results = entities.map { entity in guard let url = searchURL(for: appName, inCountry: country, ofEntity: entity) else { fatalError("Failed to build URL for \(appName)") } @@ -70,7 +70,8 @@ class MasStoreSearch: StoreSearch { } return firstly { loadSearchResults(url) - }.then { results -> Guarantee in + } + .then { results -> Guarantee in guard let result = results.first else { return .value(nil) } @@ -104,7 +105,8 @@ class MasStoreSearch: StoreSearch { private func loadSearchResults(_ url: URL) -> Promise<[SearchResult]> { firstly { networkManager.loadData(from: url) - }.map { data -> [SearchResult] in + } + .map { data in do { return try JSONDecoder().decode(SearchResultList.self, from: data).results } catch { diff --git a/Sources/mas/Formatters/Utilities.swift b/Sources/mas/Formatters/Utilities.swift index c5c940e..fae8ad3 100644 --- a/Sources/mas/Formatters/Utilities.swift +++ b/Sources/mas/Formatters/Utilities.swift @@ -69,7 +69,7 @@ func captureStream( _ stream: UnsafeMutablePointer, encoding: String.Encoding = .utf8, _ block: @escaping () throws -> Void -) throws -> String { +) rethrows -> String { let originalFd = fileno(stream) let duplicateFd = dup(originalFd) defer { diff --git a/Tests/masTests/Commands/UninstallSpec.swift b/Tests/masTests/Commands/UninstallSpec.swift index bf5890f..c6426c3 100644 --- a/Tests/masTests/Commands/UninstallSpec.swift +++ b/Tests/masTests/Commands/UninstallSpec.swift @@ -70,7 +70,7 @@ public class UninstallSpec: QuickSpec { == "==> Some App /tmp/Some.app\n==> (not removed, dry run)\n" } it("fails if there is a problem with the trash command") { - var brokenUninstall = app // make mutable copy + var brokenUninstall = app brokenUninstall.bundlePath = "/dev/null" mockLibrary.installedApps.append(brokenUninstall) expect { diff --git a/Tests/masTests/Commands/VersionSpec.swift b/Tests/masTests/Commands/VersionSpec.swift index ced12bc..0828749 100644 --- a/Tests/masTests/Commands/VersionSpec.swift +++ b/Tests/masTests/Commands/VersionSpec.swift @@ -24,7 +24,7 @@ public class VersionSpec: QuickSpec { try Mas.Version.parse([]).run() } } - == Package.version + "\n" + == "\(Package.version)\n" } } } diff --git a/Tests/masTests/Controllers/AppLibraryMock.swift b/Tests/masTests/Controllers/AppLibraryMock.swift index 4a87784..f763280 100644 --- a/Tests/masTests/Controllers/AppLibraryMock.swift +++ b/Tests/masTests/Controllers/AppLibraryMock.swift @@ -9,7 +9,7 @@ @testable import mas class AppLibraryMock: AppLibrary { - var installedApps = [SoftwareProduct]() + var installedApps: [SoftwareProduct] = [] func uninstallApp(app: SoftwareProduct) throws { if !installedApps.contains(where: { product -> Bool in diff --git a/Tests/masTests/Errors/MASErrorTestCase.swift b/Tests/masTests/Errors/MASErrorTestCase.swift index fe0248b..f9e13dd 100644 --- a/Tests/masTests/Errors/MASErrorTestCase.swift +++ b/Tests/masTests/Errors/MASErrorTestCase.swift @@ -22,7 +22,7 @@ class MASErrorTestCase: XCTestCase { var localizedDescription: String { get { "dummy value" } set { - NSError.setUserInfoValueProvider(forDomain: errorDomain) { (_: Error, _: String) -> Any? in + NSError.setUserInfoValueProvider(forDomain: errorDomain) { _, _ in newValue } } From a100f3acd0612db9248a3901f0dcf718d394fb82 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 20 Oct 2024 12:39:35 -0400 Subject: [PATCH 04/20] Remove unnecessary generics. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/Downloader.swift | 2 +- Sources/mas/AppStore/ISStoreAccount.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/mas/AppStore/Downloader.swift b/Sources/mas/AppStore/Downloader.swift index 5f90035..7e0b338 100644 --- a/Sources/mas/AppStore/Downloader.swift +++ b/Sources/mas/AppStore/Downloader.swift @@ -19,7 +19,7 @@ import StoreFoundation /// the promise is rejected with the first error, after all remaining downloads are attempted. func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise { var firstError: Error? - return appIDs.reduce(Guarantee.value(())) { previous, appID in + return appIDs.reduce(Guarantee.value(())) { previous, appID in previous.then { downloadWithRetries(appID, purchase: purchase).recover { error in if firstError == nil { diff --git a/Sources/mas/AppStore/ISStoreAccount.swift b/Sources/mas/AppStore/ISStoreAccount.swift index 7dcfd30..7758ea4 100644 --- a/Sources/mas/AppStore/ISStoreAccount.swift +++ b/Sources/mas/AppStore/ISStoreAccount.swift @@ -14,7 +14,7 @@ extension ISStoreAccount: StoreAccount { static var primaryAccount: Promise { if #available(macOS 10.13, *) { return race( - Promise { seal in + Promise { seal in ISServiceProxy.genericShared().accountService.primaryAccount { storeAccount in seal.fulfill(storeAccount) } From 3eaffa5c3e3e0926b2b1d8e6bf77b8c15e726d63 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:10:31 -0400 Subject: [PATCH 05/20] Improve DocC. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/Downloader.swift | 9 +++++---- Sources/mas/Commands/Lucky.swift | 6 ++++-- Sources/mas/Commands/Search.swift | 5 +++-- Sources/mas/Commands/Uninstall.swift | 2 +- Sources/mas/Controllers/AppLibrary.swift | 12 ++++++------ Sources/mas/Controllers/MasStoreSearch.swift | 13 +++++-------- Sources/mas/Controllers/SoftwareMap.swift | 2 +- Sources/mas/Controllers/StoreSearch.swift | 10 ++++++++-- Sources/mas/ExternalCommands/ExternalCommand.swift | 2 +- .../mas/ExternalCommands/OpenSystemCommand.swift | 3 +-- .../mas/ExternalCommands/SysCtlSystemCommand.swift | 5 +++-- Sources/mas/Formatters/SearchResultFormatter.swift | 6 ++++-- Sources/mas/Formatters/Utilities.swift | 5 +++-- Sources/mas/Models/SoftwareProduct.swift | 7 +++++-- Sources/mas/Network/NetworkManager.swift | 7 +++---- Tests/masTests/Commands/AccountSpec.swift | 2 +- Tests/masTests/Commands/SignInSpec.swift | 2 +- Tests/masTests/Errors/MASErrorTestCase.swift | 4 +++- Tests/masTests/Extensions/Bundle+JSON.swift | 3 ++- Tests/masTests/Network/NetworkSessionMock.swift | 5 ----- .../Network/NetworkSessionMockFromFile.swift | 5 ----- script/version | 2 +- 22 files changed, 61 insertions(+), 56 deletions(-) diff --git a/Sources/mas/AppStore/Downloader.swift b/Sources/mas/AppStore/Downloader.swift index 7e0b338..cb5bbc3 100644 --- a/Sources/mas/AppStore/Downloader.swift +++ b/Sources/mas/AppStore/Downloader.swift @@ -12,11 +12,12 @@ import StoreFoundation /// Downloads a list of apps, one after the other, printing progress to the console. /// -/// - Parameter appIDs: The IDs of the apps to be downloaded -/// - Parameter purchase: Flag indicating whether the apps needs to be purchased. -/// Only works for free apps. Defaults to false. +/// - Parameters: +/// - appIDs: The IDs of the apps to be downloaded +/// - purchase: Flag indicating whether the apps needs to be purchased. +/// Only works for free apps. Defaults to false. /// - Returns: A promise that completes when the downloads are complete. If any fail, -/// the promise is rejected with the first error, after all remaining downloads are attempted. +/// the promise is rejected with the first error, after all remaining downloads are attempted. func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise { var firstError: Error? return appIDs.reduce(Guarantee.value(())) { previous, appID in diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift index 3b567fd..a75fbea 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -10,8 +10,9 @@ import ArgumentParser import CommerceKit extension Mas { - /// Command which installs the first search result. This is handy as many MAS titles - /// can be long with embedded keywords. + /// Command which installs the first search result. + /// + /// This is handy as many MAS titles can be long with embedded keywords. struct Lucky: ParsableCommand { static let configuration = CommandConfiguration( abstract: "Install the first result from the Mac App Store" @@ -54,6 +55,7 @@ extension Mas { /// - Parameters: /// - appID: App identifier /// - appLibrary: Library of installed apps + /// - Throws: Any error that occurs while attempting to install the app. fileprivate func install(appID: AppID, appLibrary: AppLibrary) throws { // Try to download applications with given identifiers and collect results if let product = appLibrary.installedApp(withAppID: appID), !force { diff --git a/Sources/mas/Commands/Search.swift b/Sources/mas/Commands/Search.swift index 036c61c..79ef211 100644 --- a/Sources/mas/Commands/Search.swift +++ b/Sources/mas/Commands/Search.swift @@ -9,8 +9,9 @@ import ArgumentParser extension Mas { - /// Search the Mac App Store using the iTunes Search API: - /// https://performance-partners.apple.com/search-api + /// Search the Mac App Store using the iTunes Search API. + /// + /// See - https://performance-partners.apple.com/search-api struct Search: ParsableCommand { static let configuration = CommandConfiguration( abstract: "Search for apps from the Mac App Store" diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift index 8265296..632c84b 100644 --- a/Sources/mas/Commands/Uninstall.swift +++ b/Sources/mas/Commands/Uninstall.swift @@ -17,7 +17,7 @@ extension Mas { abstract: "Uninstall app installed from the Mac App Store" ) - /// Flag indicating that removal shouldn't be performed + /// Flag indicating that removal shouldn't be performed. @Flag(help: "dry run") var dryRun = false @Argument(help: "ID of app to uninstall") diff --git a/Sources/mas/Controllers/AppLibrary.swift b/Sources/mas/Controllers/AppLibrary.swift index 1db3ff9..ebdcd70 100644 --- a/Sources/mas/Controllers/AppLibrary.swift +++ b/Sources/mas/Controllers/AppLibrary.swift @@ -13,10 +13,10 @@ protocol AppLibrary { /// Entire set of installed apps. var installedApps: [SoftwareProduct] { get } - /// Finds an app by ID. + /// Finds an app for appID. /// - /// - Parameter withAppID: MAS ID for app. - /// - Returns: Software Product of app if found; nil otherwise. + /// - Parameter appID: app ID for app. + /// - Returns: SoftwareProduct of app if found; nil otherwise. func installedApp(withAppID appID: AppID) -> SoftwareProduct? /// Uninstalls an app. @@ -28,10 +28,10 @@ protocol AppLibrary { /// Common logic extension AppLibrary { - /// Finds an app by ID. + /// Finds an app for appID. /// - /// - Parameter withAppID: MAS ID for app. - /// - Returns: Software Product of app if found; nil otherwise. + /// - Parameter appID: app ID for app. + /// - Returns: SoftwareProduct of app if found; nil otherwise. func installedApp(withAppID appID: AppID) -> SoftwareProduct? { let appID = NSNumber(value: appID) return installedApps.first { $0.itemIdentifier == appID } diff --git a/Sources/mas/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/MasStoreSearch.swift index 6e9325a..9e64b72 100644 --- a/Sources/mas/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/MasStoreSearch.swift @@ -34,9 +34,8 @@ class MasStoreSearch: StoreSearch { /// Searches for an app. /// - /// - Parameter appName: MAS ID of app - /// - Parameter completion: A closure that receives the search results or an Error if there is a - /// problem with the network request. Results array will be empty if there were no matches. + /// - Parameter searchTerm: a search term matched against app names + /// - Returns: A Promise of an Array of SearchResults matching searchTerm func search(for appName: String) -> Promise<[SearchResult]> { // Search for apps for compatible platforms, in order of preference. // Macs with Apple Silicon can run iPad and iPhone apps. @@ -115,11 +114,9 @@ class MasStoreSearch: StoreSearch { } } - // App Store pages indicate: - // - compatibility with Macs with Apple Silicon - // - (often) a version that is newer than what is listed in search results - // - // We attempt to scrape this information here. + /// Scrape the app version from the App Store webpage at the given URL. + /// + /// App Store webpages frequently report a version that is newer than what is reported by the iTunes Search API. private func scrapeAppStoreVersion(_ pageUrl: URL) -> Promise { firstly { networkManager.loadData(from: pageUrl) diff --git a/Sources/mas/Controllers/SoftwareMap.swift b/Sources/mas/Controllers/SoftwareMap.swift index 46abea8..b131d42 100644 --- a/Sources/mas/Controllers/SoftwareMap.swift +++ b/Sources/mas/Controllers/SoftwareMap.swift @@ -6,7 +6,7 @@ // Copyright © 2020 mas-cli. All rights reserved. // -/// Somewhat analogous to CKSoftwareMap +/// Somewhat analogous to CKSoftwareMap. protocol SoftwareMap { func allSoftwareProducts() -> [SoftwareProduct] func product(for bundleIdentifier: String) -> SoftwareProduct? diff --git a/Sources/mas/Controllers/StoreSearch.swift b/Sources/mas/Controllers/StoreSearch.swift index e906cd9..12a46e0 100644 --- a/Sources/mas/Controllers/StoreSearch.swift +++ b/Sources/mas/Controllers/StoreSearch.swift @@ -40,7 +40,10 @@ private enum URLAction { extension StoreSearch { /// Builds the search URL for an app. /// - /// - Parameter searchTerm: term for which to search in MAS. + /// - Parameters: + /// - searchTerm: term for which to search in MAS. + /// - country: 2-letter ISO region code of the MAS in which to search. + /// - entity: OS platform of apps for which to search. /// - Returns: URL for the search service or nil if searchTerm can't be encoded. func searchURL( for searchTerm: String, @@ -52,7 +55,10 @@ extension StoreSearch { /// Builds the lookup URL for an app. /// - /// - Parameter appID: MAS app identifier. + /// - Parameters: + /// - appID: MAS app identifier. + /// - country: 2-letter ISO region code of the MAS in which to search. + /// - entity: OS platform of apps for which to search. /// - Returns: URL for the lookup service or nil if appID can't be encoded. func lookupURL( forAppID appID: AppID, diff --git a/Sources/mas/ExternalCommands/ExternalCommand.swift b/Sources/mas/ExternalCommands/ExternalCommand.swift index 40946be..2168a05 100644 --- a/Sources/mas/ExternalCommands/ExternalCommand.swift +++ b/Sources/mas/ExternalCommands/ExternalCommand.swift @@ -8,7 +8,7 @@ import Foundation -/// CLI command +/// Represents a CLI command. protocol ExternalCommand { var binaryPath: String { get set } diff --git a/Sources/mas/ExternalCommands/OpenSystemCommand.swift b/Sources/mas/ExternalCommands/OpenSystemCommand.swift index ebce90d..5d5e3cc 100644 --- a/Sources/mas/ExternalCommands/OpenSystemCommand.swift +++ b/Sources/mas/ExternalCommands/OpenSystemCommand.swift @@ -8,8 +8,7 @@ import Foundation -/// Wrapper for the external open system command. -/// https://ss64.com/osx/open.html +/// Wrapper for the external 'open' system command (https://ss64.com/osx/open.html). struct OpenSystemCommand: ExternalCommand { var binaryPath: String diff --git a/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift b/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift index 41d3dcb..cbb24d9 100644 --- a/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift +++ b/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift @@ -8,8 +8,9 @@ import Foundation -/// Wrapper for the external sysctl system command. -/// https://ss64.com/osx/sysctl.html +/// Wrapper for the external 'sysctl' system command. +/// +/// See - https://ss64.com/osx/sysctl.html struct SysCtlSystemCommand: ExternalCommand { var binaryPath: String diff --git a/Sources/mas/Formatters/SearchResultFormatter.swift b/Sources/mas/Formatters/SearchResultFormatter.swift index eac80a7..96e9414 100644 --- a/Sources/mas/Formatters/SearchResultFormatter.swift +++ b/Sources/mas/Formatters/SearchResultFormatter.swift @@ -10,9 +10,11 @@ import Foundation /// Formats text output for the search command. enum SearchResultFormatter { - /// Formats text output with search results. + /// Formats search results as text. /// - /// - Parameter results: Search results with app data + /// - Parameters: + /// - results: Search results containing app data + /// - includePrice: Indicates whether to include prices in the output /// - Returns: Multiline text output. static func format(results: [SearchResult], includePrice: Bool = false) -> String { // find longest appName for formatting, default 50 diff --git a/Sources/mas/Formatters/Utilities.swift b/Sources/mas/Formatters/Utilities.swift index fae8ad3..3f41341 100644 --- a/Sources/mas/Formatters/Utilities.swift +++ b/Sources/mas/Formatters/Utilities.swift @@ -8,14 +8,15 @@ import Foundation -/// A collection of output formatting helpers +// A collection of output formatting helpers -/// Terminal Control Sequence Indicator +/// Terminal Control Sequence Indicator. let csi = "\u{001B}[" private var standardError = FileHandle.standardError extension FileHandle: TextOutputStream { + /// Appends the given string to the stream. public func write(_ string: String) { guard let data = string.data(using: .utf8) else { return } write(data) diff --git a/Sources/mas/Models/SoftwareProduct.swift b/Sources/mas/Models/SoftwareProduct.swift index aa49d81..7b882c5 100644 --- a/Sources/mas/Models/SoftwareProduct.swift +++ b/Sources/mas/Models/SoftwareProduct.swift @@ -19,12 +19,15 @@ protocol SoftwareProduct { } extension SoftwareProduct { - /// Returns bundleIdentifier if appName is empty string. + /// - Returns: bundleIdentifier if appName is empty string. var appNameOrBundleIdentifier: String { appName.isEmpty ? bundleIdentifier : appName } - /// Determines whether the app is considered outdated. Updates that require a higher OS version are excluded. + /// Determines whether the app is considered outdated. + /// + /// Updates that require a higher OS version are excluded. + /// /// - Parameter storeApp: App from search result. /// - Returns: true if the app is outdated; false otherwise. func isOutdatedWhenComparedTo(_ storeApp: SearchResult) -> Bool { diff --git a/Sources/mas/Network/NetworkManager.swift b/Sources/mas/Network/NetworkManager.swift index 45fa7b8..9c4f496 100644 --- a/Sources/mas/Network/NetworkManager.swift +++ b/Sources/mas/Network/NetworkManager.swift @@ -9,11 +9,11 @@ import Foundation import PromiseKit -/// Network abstraction +/// Network abstraction. class NetworkManager { private let session: NetworkSession - /// Designated initializer + /// Designated initializer. /// /// - Parameter session: A networking session. init(session: NetworkSession = URLSession(configuration: .ephemeral)) { @@ -28,8 +28,7 @@ class NetworkManager { /// Loads data asynchronously. /// - /// - Parameters: - /// - url: URL to load data from. + /// - Parameter url: URL from which to load data. /// - Returns: A Promise for the Data of the response. func loadData(from url: URL) -> Promise { session.loadData(from: url) diff --git a/Tests/masTests/Commands/AccountSpec.swift b/Tests/masTests/Commands/AccountSpec.swift index 4cfb4da..db220c4 100644 --- a/Tests/masTests/Commands/AccountSpec.swift +++ b/Tests/masTests/Commands/AccountSpec.swift @@ -11,7 +11,7 @@ import Quick @testable import mas -// Deprecated test +/// Deprecated test. public class AccountSpec: QuickSpec { override public func spec() { beforeSuite { diff --git a/Tests/masTests/Commands/SignInSpec.swift b/Tests/masTests/Commands/SignInSpec.swift index 799d093..f7f5d33 100644 --- a/Tests/masTests/Commands/SignInSpec.swift +++ b/Tests/masTests/Commands/SignInSpec.swift @@ -11,7 +11,7 @@ import Quick @testable import mas -// Deprecated test +/// Deprecated test. public class SignInSpec: QuickSpec { override public func spec() { beforeSuite { diff --git a/Tests/masTests/Errors/MASErrorTestCase.swift b/Tests/masTests/Errors/MASErrorTestCase.swift index f9e13dd..0d59cb2 100644 --- a/Tests/masTests/Errors/MASErrorTestCase.swift +++ b/Tests/masTests/Errors/MASErrorTestCase.swift @@ -17,7 +17,9 @@ class MASErrorTestCase: XCTestCase { var nserror: NSError! /// Convenience property for setting the value which will be use for the localized description - /// value of the next NSError created. Only used when the NSError does not have a user info + /// value of the next NSError created. + /// + /// Only used when the NSError does not have a user info /// entry for localized description. var localizedDescription: String { get { "dummy value" } diff --git a/Tests/masTests/Extensions/Bundle+JSON.swift b/Tests/masTests/Extensions/Bundle+JSON.swift index 34cd6ac..3c9628c 100644 --- a/Tests/masTests/Extensions/Bundle+JSON.swift +++ b/Tests/masTests/Extensions/Bundle+JSON.swift @@ -10,7 +10,8 @@ import Foundation extension Data { /// Unsafe initializer for loading data from string paths. - /// - Parameter file: Relative path within the JSON folder + /// + /// - Parameter fileName: Relative path within the JSON folder init(from fileName: String) { let fileURL = Bundle.url(for: fileName)! try! self.init(contentsOf: fileURL, options: .mappedIfSafe) diff --git a/Tests/masTests/Network/NetworkSessionMock.swift b/Tests/masTests/Network/NetworkSessionMock.swift index a286bbb..31274cc 100644 --- a/Tests/masTests/Network/NetworkSessionMock.swift +++ b/Tests/masTests/Network/NetworkSessionMock.swift @@ -18,11 +18,6 @@ class NetworkSessionMock: NetworkSession { var data: Data? var error: Error? - /// Immediately passes data and error to completion handler. - /// - /// - Parameters: - /// - url: unused - /// - completionHandler: Closure which is delivered either data or an error. func loadData(from _: URL) -> Promise { guard let data else { return Promise(error: error ?? MASError.noData) diff --git a/Tests/masTests/Network/NetworkSessionMockFromFile.swift b/Tests/masTests/Network/NetworkSessionMockFromFile.swift index a68220d..07b25aa 100644 --- a/Tests/masTests/Network/NetworkSessionMockFromFile.swift +++ b/Tests/masTests/Network/NetworkSessionMockFromFile.swift @@ -21,11 +21,6 @@ class NetworkSessionMockFromFile: NetworkSessionMock { self.responseFile = responseFile } - /// Loads data from a file. - /// - /// - Parameters: - /// - url: unused - /// - completionHandler: Closure which is delivered either data or an error. override func loadData(from _: URL) -> Promise { guard let fileURL = Bundle.url(for: responseFile) else { fatalError("Unable to load file \(responseFile)") } diff --git a/script/version b/script/version index 2f977ff..0a3e177 100755 --- a/script/version +++ b/script/version @@ -20,7 +20,7 @@ VERSION=$(git describe --abbrev=0 --tags) VERSION=${VERSION#v} cat <"Sources/mas/Package.swift" -// Generated by: script/version +/// Generated by \`script/version\`. enum Package { static let version = "${VERSION}" } From 71fbe2e444168c715f8a2e57923c0545fd5f2682 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:12:32 -0400 Subject: [PATCH 06/20] Improve spacing. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/Downloader.swift | 24 +++++++++++-------- Sources/mas/AppStore/ISStoreAccount.swift | 21 +++++++++------- .../AppStore/PurchaseDownloadObserver.swift | 3 ++- Sources/mas/AppStore/SSPurchase.swift | 24 +++++++++---------- Sources/mas/Commands/Open.swift | 6 ++--- Sources/mas/Commands/Outdated.swift | 3 ++- Sources/mas/Commands/Upgrade.swift | 6 +++-- Sources/mas/Commands/Vendor.swift | 8 +++---- Sources/mas/Controllers/MasAppLibrary.swift | 7 +++--- Sources/mas/Controllers/MasStoreSearch.swift | 20 +++++++++------- Sources/mas/Formatters/Utilities.swift | 4 +++- .../Controllers/StoreSearchMock.swift | 3 +-- .../Network/NetworkSessionMockFromFile.swift | 5 ++-- 13 files changed, 75 insertions(+), 59 deletions(-) diff --git a/Sources/mas/AppStore/Downloader.swift b/Sources/mas/AppStore/Downloader.swift index cb5bbc3..bb09f3d 100644 --- a/Sources/mas/AppStore/Downloader.swift +++ b/Sources/mas/AppStore/Downloader.swift @@ -20,19 +20,23 @@ import StoreFoundation /// the promise is rejected with the first error, after all remaining downloads are attempted. func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise { var firstError: Error? - return appIDs.reduce(Guarantee.value(())) { previous, appID in - previous.then { - downloadWithRetries(appID, purchase: purchase).recover { error in - if firstError == nil { - firstError = error - } + return + appIDs + .reduce(Guarantee.value(())) { previous, appID in + previous.then { + downloadWithRetries(appID, purchase: purchase) + .recover { error in + if firstError == nil { + firstError = error + } + } } } - }.done { - if let error = firstError { - throw error + .done { + if let error = firstError { + throw error + } } - } } private func downloadWithRetries(_ appID: AppID, purchase: Bool = false, attempts: Int = 3) -> Promise { diff --git a/Sources/mas/AppStore/ISStoreAccount.swift b/Sources/mas/AppStore/ISStoreAccount.swift index 7758ea4..f1d4fe0 100644 --- a/Sources/mas/AppStore/ISStoreAccount.swift +++ b/Sources/mas/AppStore/ISStoreAccount.swift @@ -15,13 +15,15 @@ extension ISStoreAccount: StoreAccount { if #available(macOS 10.13, *) { return race( Promise { seal in - ISServiceProxy.genericShared().accountService.primaryAccount { storeAccount in - seal.fulfill(storeAccount) - } + ISServiceProxy.genericShared().accountService + .primaryAccount { storeAccount in + seal.fulfill(storeAccount) + } }, - after(seconds: 30).then { - Promise(error: MASError.notSignedIn) - } + after(seconds: 30) + .then { + Promise(error: MASError.notSignedIn) + } ) } else { return .value(CKAccountStore.shared().primaryAccount) @@ -76,9 +78,10 @@ extension ISStoreAccount: StoreAccount { return race( signInPromise, - after(seconds: 30).then { - Promise(error: MASError.signInFailed(error: nil)) - } + after(seconds: 30) + .then { + Promise(error: MASError.signInFailed(error: nil)) + } ) } } diff --git a/Sources/mas/AppStore/PurchaseDownloadObserver.swift b/Sources/mas/AppStore/PurchaseDownloadObserver.swift index 343b49c..fd4add4 100644 --- a/Sources/mas/AppStore/PurchaseDownloadObserver.swift +++ b/Sources/mas/AppStore/PurchaseDownloadObserver.swift @@ -9,7 +9,8 @@ import CommerceKit import StoreFoundation -@objc class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver { +@objc +class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver { let purchase: SSPurchase var completionHandler: (() -> Void)? var errorHandler: ((MASError) -> Void)? diff --git a/Sources/mas/AppStore/SSPurchase.swift b/Sources/mas/AppStore/SSPurchase.swift index cabfa51..cf712cd 100644 --- a/Sources/mas/AppStore/SSPurchase.swift +++ b/Sources/mas/AppStore/SSPurchase.swift @@ -23,7 +23,6 @@ extension SSPurchase { if purchase { parameters["macappinstalledconfirmed"] = 1 parameters["pricingParameters"] = "STDQ" - } else { parameters["pricingParameters"] = "STDRDL" } @@ -63,19 +62,20 @@ extension SSPurchase { private func perform() -> Promise { Promise { seal in - CKPurchaseController.shared().perform(self, withOptions: 0) { purchase, _, error, response in - if let error { - seal.reject(MASError.purchaseFailed(error: error as NSError?)) - return - } + CKPurchaseController.shared() + .perform(self, withOptions: 0) { purchase, _, error, response in + if let error { + seal.reject(MASError.purchaseFailed(error: error as NSError?)) + return + } - guard response?.downloads.isEmpty == false, let purchase else { - seal.reject(MASError.noDownloads) - return - } + guard response?.downloads.isEmpty == false, let purchase else { + seal.reject(MASError.noDownloads) + return + } - seal.fulfill(purchase) - } + seal.fulfill(purchase) + } } .then { purchase in let observer = PurchaseDownloadObserver(purchase: purchase) diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift index 4cc0c99..4ae4247 100644 --- a/Sources/mas/Commands/Open.swift +++ b/Sources/mas/Commands/Open.swift @@ -35,13 +35,11 @@ extension Mas { return } - guard let result = try storeSearch.lookup(appID: appID).wait() - else { + guard let result = try storeSearch.lookup(appID: appID).wait() else { throw MASError.noSearchResultsFound } - guard var url = URLComponents(string: result.trackViewUrl) - else { + guard var url = URLComponents(string: result.trackViewUrl) else { throw MASError.searchFailed } url.scheme = masScheme diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift index 9c0b1be..2164a4b 100644 --- a/Sources/mas/Commands/Outdated.swift +++ b/Sources/mas/Commands/Outdated.swift @@ -32,7 +32,8 @@ extension Mas { appLibrary.installedApps.map { installedApp in firstly { storeSearch.lookup(appID: installedApp.itemIdentifier.appIDValue) - }.done { storeApp in + } + .done { storeApp in guard let storeApp else { if verbose { printWarning( diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index cfc7b66..a588af2 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -41,7 +41,8 @@ extension Mas { print("Upgrading \(apps.count) outdated application\(apps.count > 1 ? "s" : ""):") print( apps.map { "\($0.installedApp.appName) (\($0.installedApp.bundleVersion)) -> (\($0.storeApp.version))" } - .joined(separator: "\n")) + .joined(separator: "\n") + ) do { try downloadAll(apps.map(\.installedApp.itemIdentifier.appIDValue)).wait() @@ -71,7 +72,8 @@ extension Mas { // only upgrade apps whose local version differs from the store version firstly { storeSearch.lookup(appID: installedApp.itemIdentifier.appIDValue) - }.map { result -> (SoftwareProduct, SearchResult)? in + } + .map { result -> (SoftwareProduct, SearchResult)? in guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else { return nil } diff --git a/Sources/mas/Commands/Vendor.swift b/Sources/mas/Commands/Vendor.swift index bdf13b1..7380ca0 100644 --- a/Sources/mas/Commands/Vendor.swift +++ b/Sources/mas/Commands/Vendor.swift @@ -26,13 +26,13 @@ extension Mas { func run(storeSearch: StoreSearch, openCommand: ExternalCommand) throws { do { - guard let result = try storeSearch.lookup(appID: appID).wait() - else { + guard let result = try storeSearch.lookup(appID: appID).wait() else { throw MASError.noSearchResultsFound } - guard let vendorWebsite = result.sellerUrl - else { throw MASError.noVendorWebsite } + guard let vendorWebsite = result.sellerUrl else { + throw MASError.noVendorWebsite + } do { try openCommand.run(arguments: vendorWebsite) diff --git a/Sources/mas/Controllers/MasAppLibrary.swift b/Sources/mas/Controllers/MasAppLibrary.swift index d842194..9325af4 100644 --- a/Sources/mas/Controllers/MasAppLibrary.swift +++ b/Sources/mas/Controllers/MasAppLibrary.swift @@ -14,9 +14,10 @@ class MasAppLibrary: AppLibrary { private let softwareMap: SoftwareMap /// Array of installed software products. - lazy var installedApps: [SoftwareProduct] = softwareMap.allSoftwareProducts().filter { product in - product.bundlePath.starts(with: "/Applications/") - } + lazy var installedApps: [SoftwareProduct] = softwareMap.allSoftwareProducts() + .filter { product in + product.bundlePath.starts(with: "/Applications/") + } /// Internal initializer for providing a mock software map. /// - Parameter softwareMap: SoftwareMap to use diff --git a/Sources/mas/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/MasStoreSearch.swift index 9e64b72..9680070 100644 --- a/Sources/mas/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/MasStoreSearch.swift @@ -53,9 +53,11 @@ class MasStoreSearch: StoreSearch { // Combine the results, removing any duplicates. var seenAppIDs = Set() - return when(fulfilled: results).flatMapValues { $0 }.filterValues { result in - seenAppIDs.insert(result.trackId).inserted - } + return when(fulfilled: results) + .flatMapValues { $0 } + .filterValues { result in + seenAppIDs.insert(result.trackId).inserted + } } /// Looks up app details. @@ -75,14 +77,14 @@ class MasStoreSearch: StoreSearch { return .value(nil) } - guard let pageUrl = URL(string: result.trackViewUrl) - else { + guard let pageUrl = URL(string: result.trackViewUrl) else { return .value(result) } return firstly { self.scrapeAppStoreVersion(pageUrl) - }.map { pageVersion in + } + .map { pageVersion in guard let pageVersion, let searchVersion = Version(tolerant: result.version), pageVersion > searchVersion @@ -94,7 +96,8 @@ class MasStoreSearch: StoreSearch { var result = result result.version = pageVersion.description return result - }.recover { _ in + } + .recover { _ in // If we were unable to scrape the App Store page, assume compatibility. .value(result) } @@ -120,7 +123,8 @@ class MasStoreSearch: StoreSearch { private func scrapeAppStoreVersion(_ pageUrl: URL) -> Promise { firstly { networkManager.loadData(from: pageUrl) - }.map { data in + } + .map { data in guard let html = String(data: data, encoding: .utf8), let capture = MasStoreSearch.appVersionExpression.firstMatch(in: html)?.captures[0], let version = Version(tolerant: capture) diff --git a/Sources/mas/Formatters/Utilities.swift b/Sources/mas/Formatters/Utilities.swift index 3f41341..63e436c 100644 --- a/Sources/mas/Formatters/Utilities.swift +++ b/Sources/mas/Formatters/Utilities.swift @@ -18,7 +18,9 @@ private var standardError = FileHandle.standardError extension FileHandle: TextOutputStream { /// Appends the given string to the stream. public func write(_ string: String) { - guard let data = string.data(using: .utf8) else { return } + guard let data = string.data(using: .utf8) else { + return + } write(data) } } diff --git a/Tests/masTests/Controllers/StoreSearchMock.swift b/Tests/masTests/Controllers/StoreSearchMock.swift index f97a1d3..938c2b0 100644 --- a/Tests/masTests/Controllers/StoreSearchMock.swift +++ b/Tests/masTests/Controllers/StoreSearchMock.swift @@ -18,8 +18,7 @@ class StoreSearchMock: StoreSearch { } func lookup(appID: AppID) -> Promise { - guard let result = apps[appID] - else { + guard let result = apps[appID] else { return Promise(error: MASError.noSearchResultsFound) } diff --git a/Tests/masTests/Network/NetworkSessionMockFromFile.swift b/Tests/masTests/Network/NetworkSessionMockFromFile.swift index 07b25aa..2ab1fa1 100644 --- a/Tests/masTests/Network/NetworkSessionMockFromFile.swift +++ b/Tests/masTests/Network/NetworkSessionMockFromFile.swift @@ -22,8 +22,9 @@ class NetworkSessionMockFromFile: NetworkSessionMock { } override func loadData(from _: URL) -> Promise { - guard let fileURL = Bundle.url(for: responseFile) - else { fatalError("Unable to load file \(responseFile)") } + guard let fileURL = Bundle.url(for: responseFile) else { + fatalError("Unable to load file \(responseFile)") + } do { return .value(try Data(contentsOf: fileURL, options: .mappedIfSafe)) From d7074db06f28e405793a65803c1c7719caffc93e Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:29:13 -0400 Subject: [PATCH 07/20] Remove unnecessary `else` blocks. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/ISStoreAccount.swift | 30 +++++++++++------------ Sources/mas/Commands/Upgrade.swift | 8 +++--- Sources/mas/Errors/MASError.swift | 18 +++++--------- 3 files changed, 25 insertions(+), 31 deletions(-) diff --git a/Sources/mas/AppStore/ISStoreAccount.swift b/Sources/mas/AppStore/ISStoreAccount.swift index f1d4fe0..414dc29 100644 --- a/Sources/mas/AppStore/ISStoreAccount.swift +++ b/Sources/mas/AppStore/ISStoreAccount.swift @@ -25,9 +25,9 @@ extension ISStoreAccount: StoreAccount { Promise(error: MASError.notSignedIn) } ) - } else { - return .value(CKAccountStore.shared().primaryAccount) } + + return .value(CKAccountStore.shared().primaryAccount) } static func signIn(username: String, password: String, systemDialog: Bool) -> Promise { @@ -70,20 +70,20 @@ extension ISStoreAccount: StoreAccount { if systemDialog { return signInPromise - } else { - context.demoMode = true - context.demoAccountName = username - context.demoAccountPassword = password - context.demoAutologinMode = true - - return race( - signInPromise, - after(seconds: 30) - .then { - Promise(error: MASError.signInFailed(error: nil)) - } - ) } + + context.demoMode = true + context.demoAccountName = username + context.demoAccountPassword = password + context.demoAutologinMode = true + + return race( + signInPromise, + after(seconds: 30) + .then { + Promise(error: MASError.signInFailed(error: nil)) + } + ) } } } diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index a588af2..bd23d93 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -60,12 +60,12 @@ extension Mas { ? appLibrary.installedApps : appIDs.compactMap { if let appID = AppID($0) { - // if argument an AppID, lookup app by id using argument + // argument is an AppID, lookup app by id using argument return appLibrary.installedApp(withAppID: appID) - } else { - // if argument not an AppID, lookup app by name using argument - return appLibrary.installedApp(named: $0) } + + // argument is not an AppID, lookup app by name using argument + return appLibrary.installedApp(named: $0) } let promises = apps.map { installedApp in diff --git a/Sources/mas/Errors/MASError.swift b/Sources/mas/Errors/MASError.swift index 4d4155a..28e6754 100644 --- a/Sources/mas/Errors/MASError.swift +++ b/Sources/mas/Errors/MASError.swift @@ -51,29 +51,25 @@ extension MASError: CustomStringConvertible { case .failed(let error): if let error { return "Failed: \(error.localizedDescription)" - } else { - return "Failed" } + return "Failed" case .signInFailed(let error): if let error { return "Sign in failed: \(error.localizedDescription)" - } else { - return "Sign in failed" } + return "Sign in failed" case .alreadySignedIn(let accountId): return "Already signed in as \(accountId)" case .purchaseFailed(let error): if let error { return "Download request failed: \(error.localizedDescription)" - } else { - return "Download request failed" } + return "Download request failed" case .downloadFailed(let error): if let error { return "Download failed: \(error.localizedDescription)" - } else { - return "Download failed" } + return "Download failed" case .noDownloads: return "No downloads began" case .cancelled: @@ -94,12 +90,10 @@ extension MASError: CustomStringConvertible { if let data { if let unparsable = String(data: data, encoding: .utf8) { return "Unable to parse response as JSON: \n\(unparsable)" - } else { - return "Received defective response" } - } else { - return "Received empty response" + return "Received defective response" } + return "Received empty response" } } } From 06a347c4501f94ad2ba528f07134d9d01319379b Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:35:16 -0400 Subject: [PATCH 08/20] Use `isEmpty` / `beEmpty()`. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Upgrade.swift | 2 +- Tests/masTests/Formatters/AppListFormatterSpec.swift | 2 +- Tests/masTests/Formatters/SearchResultFormatterSpec.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index bd23d93..db825b6 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -33,7 +33,7 @@ extension Mas { throw error as? MASError ?? .searchFailed } - guard apps.count > 0 else { + guard !apps.isEmpty else { printWarning("Nothing found to upgrade") return } diff --git a/Tests/masTests/Formatters/AppListFormatterSpec.swift b/Tests/masTests/Formatters/AppListFormatterSpec.swift index 98ed50f..42346ae 100644 --- a/Tests/masTests/Formatters/AppListFormatterSpec.swift +++ b/Tests/masTests/Formatters/AppListFormatterSpec.swift @@ -25,7 +25,7 @@ public class AppListsFormatterSpec: QuickSpec { products = [] } it("formats nothing as empty string") { - expect(format(products)) == "" + expect(format(products)).to(beEmpty()) } it("can format a single product") { let product = SoftwareProductMock( diff --git a/Tests/masTests/Formatters/SearchResultFormatterSpec.swift b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift index f92731d..10fec4b 100644 --- a/Tests/masTests/Formatters/SearchResultFormatterSpec.swift +++ b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift @@ -25,7 +25,7 @@ public class SearchResultsFormatterSpec: QuickSpec { results = [] } it("formats nothing as empty string") { - expect(format(results, false)) == "" + expect(format(results, false)).to(beEmpty()) } it("can format a single result") { let result = SearchResult( From e927466dceb9801176e8d7b6f3463f7644b0ca2c Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:38:04 -0400 Subject: [PATCH 09/20] Use `Self`. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Controllers/MasStoreSearch.swift | 2 +- Sources/mas/ExternalCommands/SysCtlSystemCommand.swift | 2 +- Sources/mas/Mas.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/mas/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/MasStoreSearch.swift index 9680070..a2cf5b2 100644 --- a/Sources/mas/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/MasStoreSearch.swift @@ -126,7 +126,7 @@ class MasStoreSearch: StoreSearch { } .map { data in guard let html = String(data: data, encoding: .utf8), - let capture = MasStoreSearch.appVersionExpression.firstMatch(in: html)?.captures[0], + let capture = Self.appVersionExpression.firstMatch(in: html)?.captures[0], let version = Version(tolerant: capture) else { return nil diff --git a/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift b/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift index cbb24d9..abd5fb8 100644 --- a/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift +++ b/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift @@ -24,7 +24,7 @@ struct SysCtlSystemCommand: ExternalCommand { } static var isAppleSilicon: Bool = { - let sysctl = SysCtlSystemCommand() + let sysctl = Self() do { // Returns 1 on Apple Silicon even when run in an Intel context in Rosetta. try sysctl.run(arguments: "-in", "hw.optional.arm64") diff --git a/Sources/mas/Mas.swift b/Sources/mas/Mas.swift index 1d273fa..5fb8dda 100644 --- a/Sources/mas/Mas.swift +++ b/Sources/mas/Mas.swift @@ -36,7 +36,7 @@ struct Mas: ParsableCommand { ) func validate() throws { - Mas.initialize() + Self.initialize() } static func initialize() { From 490ee2d3385f8cf854923ebaf8b887742c916708 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:38:51 -0400 Subject: [PATCH 10/20] Fix class names. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Tests/masTests/Formatters/AppListFormatterSpec.swift | 2 +- Tests/masTests/Formatters/SearchResultFormatterSpec.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/masTests/Formatters/AppListFormatterSpec.swift b/Tests/masTests/Formatters/AppListFormatterSpec.swift index 42346ae..47b3b18 100644 --- a/Tests/masTests/Formatters/AppListFormatterSpec.swift +++ b/Tests/masTests/Formatters/AppListFormatterSpec.swift @@ -11,7 +11,7 @@ import Quick @testable import mas -public class AppListsFormatterSpec: QuickSpec { +public class AppListFormatterSpec: QuickSpec { override public func spec() { // static func reference let format = AppListFormatter.format(products:) diff --git a/Tests/masTests/Formatters/SearchResultFormatterSpec.swift b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift index 10fec4b..1689fa0 100644 --- a/Tests/masTests/Formatters/SearchResultFormatterSpec.swift +++ b/Tests/masTests/Formatters/SearchResultFormatterSpec.swift @@ -11,7 +11,7 @@ import Quick @testable import mas -public class SearchResultsFormatterSpec: QuickSpec { +public class SearchResultFormatterSpec: QuickSpec { override public func spec() { // static func reference let format = SearchResultFormatter.format(results:includePrice:) From 586de073892a6a5f704d1dca3654f1e94a60e97c Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:40:14 -0400 Subject: [PATCH 11/20] Improve access control. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Lucky.swift | 2 +- Tests/masTests/Errors/MASErrorTestCase.swift | 6 +++--- Tests/masTests/Network/NetworkManagerTests.swift | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift index a75fbea..b79c776 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -56,7 +56,7 @@ extension Mas { /// - appID: App identifier /// - appLibrary: Library of installed apps /// - Throws: Any error that occurs while attempting to install the app. - fileprivate func install(appID: AppID, appLibrary: AppLibrary) throws { + private func install(appID: AppID, appLibrary: AppLibrary) throws { // Try to download applications with given identifiers and collect results if let product = appLibrary.installedApp(withAppID: appID), !force { printWarning("\(product.appName) is already installed") diff --git a/Tests/masTests/Errors/MASErrorTestCase.swift b/Tests/masTests/Errors/MASErrorTestCase.swift index 0d59cb2..fdc044e 100644 --- a/Tests/masTests/Errors/MASErrorTestCase.swift +++ b/Tests/masTests/Errors/MASErrorTestCase.swift @@ -13,15 +13,15 @@ import XCTest class MASErrorTestCase: XCTestCase { private let errorDomain = "MAS" - var error: MASError! - var nserror: NSError! + private var error: MASError! + private var nserror: NSError! /// Convenience property for setting the value which will be use for the localized description /// value of the next NSError created. /// /// Only used when the NSError does not have a user info /// entry for localized description. - var localizedDescription: String { + private var localizedDescription: String { get { "dummy value" } set { NSError.setUserInfoValueProvider(forDomain: errorDomain) { _, _ in diff --git a/Tests/masTests/Network/NetworkManagerTests.swift b/Tests/masTests/Network/NetworkManagerTests.swift index a92227c..450bb39 100644 --- a/Tests/masTests/Network/NetworkManagerTests.swift +++ b/Tests/masTests/Network/NetworkManagerTests.swift @@ -11,7 +11,7 @@ import XCTest @testable import mas class NetworkManagerTests: XCTestCase { - override public func setUp() { + override func setUp() { super.setUp() Mas.initialize() } From 2af2a42a88893f4173796dc429d667d2fad71c1f Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:42:30 -0400 Subject: [PATCH 12/20] Improve parameter names. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Upgrade.swift | 6 +++--- Sources/mas/Controllers/MasStoreSearch.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index db825b6..41054d2 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -58,14 +58,14 @@ extension Mas { let apps: [SoftwareProduct] = appIDs.isEmpty ? appLibrary.installedApps - : appIDs.compactMap { - if let appID = AppID($0) { + : appIDs.compactMap { appID in + if let appID = AppID(appID) { // argument is an AppID, lookup app by id using argument return appLibrary.installedApp(withAppID: appID) } // argument is not an AppID, lookup app by name using argument - return appLibrary.installedApp(named: $0) + return appLibrary.installedApp(named: appID) } let promises = apps.map { installedApp in diff --git a/Sources/mas/Controllers/MasStoreSearch.swift b/Sources/mas/Controllers/MasStoreSearch.swift index a2cf5b2..2dc7ff4 100644 --- a/Sources/mas/Controllers/MasStoreSearch.swift +++ b/Sources/mas/Controllers/MasStoreSearch.swift @@ -36,7 +36,7 @@ class MasStoreSearch: StoreSearch { /// /// - Parameter searchTerm: a search term matched against app names /// - Returns: A Promise of an Array of SearchResults matching searchTerm - func search(for appName: String) -> Promise<[SearchResult]> { + func search(for searchTerm: String) -> Promise<[SearchResult]> { // Search for apps for compatible platforms, in order of preference. // Macs with Apple Silicon can run iPad and iPhone apps. var entities = [Entity.desktopSoftware] @@ -45,8 +45,8 @@ class MasStoreSearch: StoreSearch { } let results = entities.map { entity in - guard let url = searchURL(for: appName, inCountry: country, ofEntity: entity) else { - fatalError("Failed to build URL for \(appName)") + guard let url = searchURL(for: searchTerm, inCountry: country, ofEntity: entity) else { + fatalError("Failed to build URL for \(searchTerm)") } return loadSearchResults(url) } From 3a6d6724c9a2743e02581881e97be77d099bd42e Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:43:13 -0400 Subject: [PATCH 13/20] Reorder elements. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../SysCtlSystemCommand.swift | 22 +++++++++---------- Sources/mas/Mas.swift | 8 +++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift b/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift index abd5fb8..1d04a2e 100644 --- a/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift +++ b/Sources/mas/ExternalCommands/SysCtlSystemCommand.swift @@ -12,17 +12,6 @@ import Foundation /// /// See - https://ss64.com/osx/sysctl.html struct SysCtlSystemCommand: ExternalCommand { - var binaryPath: String - - let process = Process() - - let stdoutPipe = Pipe() - let stderrPipe = Pipe() - - init(binaryPath: String = "/usr/sbin/sysctl") { - self.binaryPath = binaryPath - } - static var isAppleSilicon: Bool = { let sysctl = Self() do { @@ -38,4 +27,15 @@ struct SysCtlSystemCommand: ExternalCommand { return sysctl.stdout.trimmingCharacters(in: .newlines) == "1" }() + + let process = Process() + + let stdoutPipe = Pipe() + let stderrPipe = Pipe() + + var binaryPath: String + + init(binaryPath: String = "/usr/sbin/sysctl") { + self.binaryPath = binaryPath + } } diff --git a/Sources/mas/Mas.swift b/Sources/mas/Mas.swift index 5fb8dda..89aeacb 100644 --- a/Sources/mas/Mas.swift +++ b/Sources/mas/Mas.swift @@ -35,10 +35,6 @@ struct Mas: ParsableCommand { ] ) - func validate() throws { - Self.initialize() - } - static func initialize() { PromiseKit.conf.Q.map = .global() PromiseKit.conf.Q.return = .global() @@ -54,6 +50,10 @@ struct Mas: ParsableCommand { } } } + + func validate() throws { + Self.initialize() + } } typealias AppID = UInt64 From 65218eff7485b89ab678160ba338d588cf909289 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:43:45 -0400 Subject: [PATCH 14/20] Comment that catch ignores error. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Network/NetworkManager.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/mas/Network/NetworkManager.swift b/Sources/mas/Network/NetworkManager.swift index 9c4f496..5f0ab66 100644 --- a/Sources/mas/Network/NetworkManager.swift +++ b/Sources/mas/Network/NetworkManager.swift @@ -23,7 +23,9 @@ class NetworkManager { do { let url = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Library/Caches/com.mphys.mas-cli") try FileManager.default.removeItem(at: url) - } catch {} + } catch { + // Ignore + } } /// Loads data asynchronously. From fcf8ba29f6ff2acfefd40203840c571bef91ff6b Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:41:22 -0400 Subject: [PATCH 15/20] Improve error output. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Lucky.swift | 2 +- Sources/mas/Commands/Reset.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift index b79c776..16c7948 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -44,7 +44,7 @@ extension Mas { } guard let appID else { - fatalError() + fatalError("app ID returned from Apple is null") } try install(appID: appID, appLibrary: appLibrary) diff --git a/Sources/mas/Commands/Reset.swift b/Sources/mas/Commands/Reset.swift index 0ddc210..6698780 100644 --- a/Sources/mas/Commands/Reset.swift +++ b/Sources/mas/Commands/Reset.swift @@ -61,7 +61,7 @@ extension Mas { if kill.terminationStatus != 0, debug { let output = stderr.fileHandleForReading.readDataToEndOfFile() - printInfo("killall failed:\r\n\(String(data: output, encoding: String.Encoding.utf8)!)") + printError("killall failed:\n\(String(data: output, encoding: String.Encoding.utf8)!)") } // Wipe Download Directory From ee28af69df9dc419ad8b817a4e536fbcbeba9a31 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 08:58:18 -0400 Subject: [PATCH 16/20] Eliminate magic numbers. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/ISStoreAccount.swift | 6 ++++-- Sources/mas/AppStore/PurchaseDownloadObserver.swift | 1 + Sources/mas/Formatters/SearchResultFormatter.swift | 6 ++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Sources/mas/AppStore/ISStoreAccount.swift b/Sources/mas/AppStore/ISStoreAccount.swift index 414dc29..1c8afa0 100644 --- a/Sources/mas/AppStore/ISStoreAccount.swift +++ b/Sources/mas/AppStore/ISStoreAccount.swift @@ -10,6 +10,8 @@ import CommerceKit import PromiseKit import StoreFoundation +private let timeout = 30.0 + extension ISStoreAccount: StoreAccount { static var primaryAccount: Promise { if #available(macOS 10.13, *) { @@ -20,7 +22,7 @@ extension ISStoreAccount: StoreAccount { seal.fulfill(storeAccount) } }, - after(seconds: 30) + after(seconds: timeout) .then { Promise(error: MASError.notSignedIn) } @@ -79,7 +81,7 @@ extension ISStoreAccount: StoreAccount { return race( signInPromise, - after(seconds: 30) + after(seconds: timeout) .then { Promise(error: MASError.signInFailed(error: nil)) } diff --git a/Sources/mas/AppStore/PurchaseDownloadObserver.swift b/Sources/mas/AppStore/PurchaseDownloadObserver.swift index fd4add4..c6709cf 100644 --- a/Sources/mas/AppStore/PurchaseDownloadObserver.swift +++ b/Sources/mas/AppStore/PurchaseDownloadObserver.swift @@ -65,6 +65,7 @@ struct ProgressState { let phase: String var percentage: String { + // swiftlint:disable:next no_magic_numbers String(format: "%.1f%%", floor(percentComplete * 1000) / 10) } } diff --git a/Sources/mas/Formatters/SearchResultFormatter.swift b/Sources/mas/Formatters/SearchResultFormatter.swift index 96e9414..7145d87 100644 --- a/Sources/mas/Formatters/SearchResultFormatter.swift +++ b/Sources/mas/Formatters/SearchResultFormatter.swift @@ -17,8 +17,10 @@ enum SearchResultFormatter { /// - includePrice: Indicates whether to include prices in the output /// - Returns: Multiline text output. static func format(results: [SearchResult], includePrice: Bool = false) -> String { - // find longest appName for formatting, default 50 - let maxLength = results.map(\.trackName.count).max() ?? 50 + guard let maxLength = results.map(\.trackName.count).max() else { + return "" + } + var output = "" for result in results { From 87aaba555db65cc3353a059c78502f17c88eeb49 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 22 Oct 2024 13:39:27 -0400 Subject: [PATCH 17/20] Remove unnecessary variable type. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/Commands/Upgrade.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index 41054d2..4b3740b 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -55,7 +55,7 @@ extension Mas { appLibrary: AppLibrary, storeSearch: StoreSearch ) throws -> [(SoftwareProduct, SearchResult)] { - let apps: [SoftwareProduct] = + let apps = appIDs.isEmpty ? appLibrary.installedApps : appIDs.compactMap { appID in From 2edd21803bc40679d989cc28e06dc31e4b89e3f3 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Wed, 23 Oct 2024 01:32:16 -0400 Subject: [PATCH 18/20] Improve unwrapping. Cleanup code. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/ISStoreAccount.swift | 2 +- Sources/mas/Commands/Open.swift | 9 ++++++--- Sources/mas/Commands/Reset.swift | 2 +- Sources/mas/Controllers/StoreSearch.swift | 8 +++++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Sources/mas/AppStore/ISStoreAccount.swift b/Sources/mas/AppStore/ISStoreAccount.swift index 1c8afa0..f0d27ec 100644 --- a/Sources/mas/AppStore/ISStoreAccount.swift +++ b/Sources/mas/AppStore/ISStoreAccount.swift @@ -47,7 +47,7 @@ extension ISStoreAccount: StoreAccount { let password = password.isEmpty && !systemDialog - ? String(validatingUTF8: getpass("Password: "))! + ? String(validatingUTF8: getpass("Password: ")) ?? "" : password guard !password.isEmpty || systemDialog else { diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift index 4ae4247..82e98de 100644 --- a/Sources/mas/Commands/Open.swift +++ b/Sources/mas/Commands/Open.swift @@ -44,15 +44,18 @@ extension Mas { } url.scheme = masScheme + guard let urlString = url.string else { + printError("Unable to construct URL") + throw MASError.searchFailed + } do { - try openCommand.run(arguments: url.string!) + try openCommand.run(arguments: urlString) } catch { printError("Unable to launch open command") throw MASError.searchFailed } if openCommand.failed { - let reason = openCommand.process.terminationReason - printError("Open failed: (\(reason)) \(openCommand.stderr)") + printError("Open failed: (\(openCommand.process.terminationReason)) \(openCommand.stderr)") throw MASError.searchFailed } } catch { diff --git a/Sources/mas/Commands/Reset.swift b/Sources/mas/Commands/Reset.swift index 6698780..76a32bd 100644 --- a/Sources/mas/Commands/Reset.swift +++ b/Sources/mas/Commands/Reset.swift @@ -61,7 +61,7 @@ extension Mas { if kill.terminationStatus != 0, debug { let output = stderr.fileHandleForReading.readDataToEndOfFile() - printError("killall failed:\n\(String(data: output, encoding: String.Encoding.utf8)!)") + printError("killall failed:\n\(String(data: output, encoding: .utf8) ?? "Error info not available")") } // Wipe Download Directory diff --git a/Sources/mas/Controllers/StoreSearch.swift b/Sources/mas/Controllers/StoreSearch.swift index 12a46e0..166e7fd 100644 --- a/Sources/mas/Controllers/StoreSearch.swift +++ b/Sources/mas/Controllers/StoreSearch.swift @@ -78,16 +78,18 @@ extension StoreSearch { return nil } - components.queryItems = [ + var queryItems = [ URLQueryItem(name: "media", value: "software"), URLQueryItem(name: "entity", value: entity.rawValue), ] if let country { - components.queryItems!.append(URLQueryItem(name: "country", value: country)) + queryItems.append(URLQueryItem(name: "country", value: country)) } - components.queryItems!.append(URLQueryItem(name: action.queryItemName, value: queryItemValue)) + queryItems.append(URLQueryItem(name: action.queryItemName, value: queryItemValue)) + + components.queryItems = queryItems return components.url } From 4d4dd02cd3d68d5ada3d72388176f08e0173f9aa Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:47:39 -0400 Subject: [PATCH 19/20] Improve lint configurations. Partial #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .swift-format | 95 ++++++++++------ .swiftformat | 21 +++- .swiftlint.yml | 132 +++++++++++++++++++++- Sources/mas/AppStore/ISStoreAccount.swift | 2 + Tests/masTests/.swift-format | 62 ++++++++++ Tests/masTests/.swiftlint.yml | 2 + 6 files changed, 266 insertions(+), 48 deletions(-) create mode 100644 Tests/masTests/.swift-format diff --git a/.swift-format b/.swift-format index 9c12fd6..693d879 100644 --- a/.swift-format +++ b/.swift-format @@ -1,41 +1,62 @@ { - "indentation" : { - "spaces" : 4 + "indentConditionalCompilationBlocks": false, + "indentation": { + "spaces": 4 }, - "lineLength" : 120, - "rules" : { - "AllPublicDeclarationsHaveDocumentation" : false, - "AlwaysUseLowerCamelCase" : true, - "AmbiguousTrailingClosureOverload" : true, - "BeginDocumentationCommentWithOneLineSummary" : false, - "DoNotUseSemicolons" : true, - "DontRepeatTypeInStaticProperties" : true, - "FileScopedDeclarationPrivacy" : true, - "FullyIndirectEnum" : true, - "GroupNumericLiterals" : true, - "IdentifiersMustBeASCII" : true, - "NeverForceUnwrap" : false, - "NeverUseForceTry" : false, - "NeverUseImplicitlyUnwrappedOptionals" : false, - "NoAccessLevelOnExtensionDeclaration" : false, - "NoBlockComments" : true, - "NoCasesWithOnlyFallthrough" : true, - "NoEmptyTrailingClosureParentheses" : true, - "NoLabelsInCasePatterns" : true, - "NoLeadingUnderscores" : false, - "NoParensAroundConditions" : true, - "NoVoidReturnOnFunctionSignature" : true, - "OneCasePerLine" : true, - "OneVariableDeclarationPerLine" : true, - "OnlyOneTrailingClosureArgument" : true, - "OrderedImports" : true, - "ReturnVoidInsteadOfEmptyTuple" : true, - "UseLetInEveryBoundCaseVariable" : true, - "UseShorthandTypeNames" : true, - "UseSingleLinePropertyGetter" : true, - "UseSynthesizedInitializer" : true, - "UseTripleSlashForDocumentationComments" : true, - "ValidateDocumentationComments" : false + "lineBreakAroundMultilineExpressionChainComponents": true, + "lineBreakBeforeControlFlowKeywords": false, + "lineBreakBeforeEachArgument": true, + "lineBreakBeforeEachGenericRequirement": true, + "lineBreakBetweenDeclarationAttributes": true, + "lineLength": 120, + "maximumBlankLines": 1, + "multiElementCollectionTrailingCommas": true, + "prioritizeKeepingFunctionOutputTogether": true, + "respectsExistingLineBreaks": true, + "rules": { + "AllPublicDeclarationsHaveDocumentation": true, + "AlwaysUseLiteralForEmptyCollectionInit": true, + "AlwaysUseLowerCamelCase": true, + "AmbiguousTrailingClosureOverload": true, + "BeginDocumentationCommentWithOneLineSummary": true, + "DoNotUseSemicolons": true, + "DontRepeatTypeInStaticProperties": true, + "FileScopedDeclarationPrivacy": true, + "FullyIndirectEnum": true, + "GroupNumericLiterals": true, + "IdentifiersMustBeASCII": true, + "NeverForceUnwrap": true, + "NeverUseForceTry": true, + "NeverUseImplicitlyUnwrappedOptionals": true, + "NoAccessLevelOnExtensionDeclaration": true, + "NoAssignmentInExpressions": true, + "NoBlockComments": true, + "NoCasesWithOnlyFallthrough": true, + "NoEmptyTrailingClosureParentheses": true, + "NoLabelsInCasePatterns": true, + "NoLeadingUnderscores": true, + "NoParensAroundConditions": true, + "NoPlaygroundLiterals": true, + "NoVoidReturnOnFunctionSignature": true, + "OmitExplicitReturns": true, + "OneCasePerLine": true, + "OneVariableDeclarationPerLine": true, + "OnlyOneTrailingClosureArgument": true, + "OrderedImports": true, + "ReplaceForEachWithForLoop": true, + "ReturnVoidInsteadOfEmptyTuple": true, + "TypeNamesShouldBeCapitalized": true, + "UseEarlyExits": true, + "UseLetInEveryBoundCaseVariable": true, + "UseShorthandTypeNames": true, + "UseSingleLinePropertyGetter": true, + "UseSynthesizedInitializer": true, + "UseTripleSlashForDocumentationComments": true, + "UseWhereClausesInForLoops": true, + "ValidateDocumentationComments": true }, - "version" : 1 + "spacesAroundRangeFormationOperators": false, + "spacesBeforeEndOfLineComments": 1, + "TrailingComma": false, + "version": 1 } diff --git a/.swiftformat b/.swiftformat index 9e09924..90ad275 100644 --- a/.swiftformat +++ b/.swiftformat @@ -5,22 +5,33 @@ # https://github.com/nicklockwood/SwiftFormat#config-file # ---exclude docs/ - # Disabled rules ---disable blankLinesAroundMark ---disable consecutiveSpaces --disable hoistAwait --disable hoistPatternLet --disable hoistTry + +# Enable later --disable indent --disable trailingCommas # Enabled rules (disabled by default) ---enable trailingClosures +#--enable acronyms +#--enable blankLinesBetweenImports +--enable blockComments +--enable docComments +--enable isEmpty +--enable noExplicitOwnership +#--enable organizeDeclarations +--enable redundantProperty +--enable sortSwitchCases +--enable wrapConditionalBodies +--enable wrapEnumCases +--enable wrapMultilineConditionalAssignment +--enable wrapSwitchCases # Rule options --commas always --extensionacl on-declarations --importgrouping testable-last +--lineaftermarks false --ranges no-space diff --git a/.swiftlint.yml b/.swiftlint.yml index 1c9c9e3..4897eef 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -5,11 +5,131 @@ # https://github.com/realm/SwiftLint#configuration # --- +opt_in_rules: +- accessibility_label_for_image +- accessibility_trait_for_button +- anonymous_argument_in_multiline_closure +- array_init +- attributes +- closure_end_indentation +- closure_spacing +- collection_alignment +- comma_inheritance +- conditional_returns_on_newline +- contains_over_filter_count +- contains_over_filter_is_empty +- contains_over_first_not_nil +- contains_over_range_nil_comparison +- convenience_type +- direct_return +- discarded_notification_center_observer +- discouraged_assert +- discouraged_none_name +- discouraged_object_literal +- discouraged_optional_boolean +- discouraged_optional_collection +- empty_collection_literal +- empty_count +- empty_string +- empty_xctest_method +- enum_case_associated_values_count +- expiring_todo +- explicit_init +- extension_access_modifier +- fallthrough +- fatal_error_message +- file_name_no_space +- file_types_order +- first_where +- flatmap_over_map_reduce +- function_default_parameter_at_end +- ibinspectable_in_extension +- identical_operands +- implicit_return +- implicitly_unwrapped_optional +- indentation_width +- joined_default_parameter +- last_where +- legacy_multiple +- let_var_whitespace +- literal_expression_end_indentation +- local_doc_comment +- lower_acl_than_parent +- missing_docs +- modifier_order +- multiline_arguments +- multiline_arguments_brackets +- multiline_function_chains +- multiline_literal_brackets +- multiline_parameters +- multiline_parameters_brackets +- nimble_operator +- no_empty_block +- no_extension_access_modifier +- no_magic_numbers +- non_overridable_class_declaration +- nslocalizedstring_key +- nslocalizedstring_require_bundle +- object_literal +- operator_usage_whitespace +- optional_enum_case_matching +- overridden_super_call +- override_in_extension +- pattern_matching_keywords +- period_spacing +- prefer_key_path +- prefer_self_in_static_references +- prefer_self_type_over_type_of_self +- prefer_zero_over_explicit_init +- private_action +- private_outlet +- private_subject +- private_swiftui_state +- prohibited_interface_builder +- prohibited_super_call +- quick_discouraged_focused_test +- raw_value_for_camel_cased_codable_enum +- reduce_into +- redundant_nil_coalescing +- redundant_self_in_closure +- redundant_type_annotation +- required_enum_case +- return_value_from_void_function +- self_binding +- shorthand_argument +- shorthand_optional_binding +- single_test_class +- sorted_first_last +- sorted_imports +- static_operator +- strict_fileprivate +- strong_iboutlet +- superfluous_else +- switch_case_on_newline +- test_case_accessibility +- toggle_bool +- trailing_closure +- type_contents_order +- unavailable_function +- unhandled_throwing_task +- unneeded_parentheses_in_closure_argument +- unowned_variable_capture +- untyped_error_in_catch +- unused_parameter +- vertical_parameter_alignment_on_call +- vertical_whitespace_closing_braces +- vertical_whitespace_opening_braces +- weak_delegate +- xct_specific_matcher +- yoda_condition disabled_rules: -- non_optional_string_data_conversion +- function_body_length - trailing_comma -excluded: -- docs -opening_brace: - ignore_multiline_function_signatures: true - ignore_multiline_statement_conditions: true +file_types_order: + order: [ + [main_type], + [supporting_type], + [extension], + [preview_provider], + [library_content_provider] + ] diff --git a/Sources/mas/AppStore/ISStoreAccount.swift b/Sources/mas/AppStore/ISStoreAccount.swift index f0d27ec..ecb3313 100644 --- a/Sources/mas/AppStore/ISStoreAccount.swift +++ b/Sources/mas/AppStore/ISStoreAccount.swift @@ -33,10 +33,12 @@ extension ISStoreAccount: StoreAccount { } static func signIn(username: String, password: String, systemDialog: Bool) -> Promise { + // swift-format-ignore: UseEarlyExits if #available(macOS 10.13, *) { // Signing in is no longer possible as of High Sierra. // https://github.com/mas-cli/mas/issues/164 return Promise(error: MASError.notSupported) + // swiftlint:disable:next superfluous_else } else { return primaryAccount diff --git a/Tests/masTests/.swift-format b/Tests/masTests/.swift-format new file mode 100644 index 0000000..571f02f --- /dev/null +++ b/Tests/masTests/.swift-format @@ -0,0 +1,62 @@ +{ + "indentConditionalCompilationBlocks": false, + "indentation": { + "spaces": 4 + }, + "lineBreakAroundMultilineExpressionChainComponents": true, + "lineBreakBeforeControlFlowKeywords": false, + "lineBreakBeforeEachArgument": true, + "lineBreakBeforeEachGenericRequirement": true, + "lineBreakBetweenDeclarationAttributes": true, + "lineLength": 120, + "maximumBlankLines": 1, + "multiElementCollectionTrailingCommas": true, + "prioritizeKeepingFunctionOutputTogether": true, + "respectsExistingLineBreaks": true, + "rules": { + "AllPublicDeclarationsHaveDocumentation": false, + "AlwaysUseLiteralForEmptyCollectionInit": true, + "AlwaysUseLowerCamelCase": true, + "AmbiguousTrailingClosureOverload": true, + "BeginDocumentationCommentWithOneLineSummary": true, + "DoNotUseSemicolons": true, + "DontRepeatTypeInStaticProperties": true, + "FileScopedDeclarationPrivacy": true, + "FullyIndirectEnum": true, + "GroupNumericLiterals": true, + "IdentifiersMustBeASCII": true, + "NeverForceUnwrap": false, + "NeverUseForceTry": false, + "NeverUseImplicitlyUnwrappedOptionals": true, + "NoAccessLevelOnExtensionDeclaration": true, + "NoAssignmentInExpressions": true, + "NoBlockComments": true, + "NoCasesWithOnlyFallthrough": true, + "NoEmptyTrailingClosureParentheses": true, + "NoLabelsInCasePatterns": true, + "NoLeadingUnderscores": true, + "NoParensAroundConditions": true, + "NoPlaygroundLiterals": true, + "NoVoidReturnOnFunctionSignature": true, + "OmitExplicitReturns": true, + "OneCasePerLine": true, + "OneVariableDeclarationPerLine": true, + "OnlyOneTrailingClosureArgument": true, + "OrderedImports": true, + "ReplaceForEachWithForLoop": true, + "ReturnVoidInsteadOfEmptyTuple": true, + "TypeNamesShouldBeCapitalized": true, + "UseEarlyExits": true, + "UseLetInEveryBoundCaseVariable": true, + "UseShorthandTypeNames": true, + "UseSingleLinePropertyGetter": true, + "UseSynthesizedInitializer": true, + "UseTripleSlashForDocumentationComments": true, + "UseWhereClausesInForLoops": true, + "ValidateDocumentationComments": true + }, + "spacesAroundRangeFormationOperators": false, + "spacesBeforeEndOfLineComments": 1, + "TrailingComma": false, + "version": 1 +} diff --git a/Tests/masTests/.swiftlint.yml b/Tests/masTests/.swiftlint.yml index d234b91..409fabb 100644 --- a/Tests/masTests/.swiftlint.yml +++ b/Tests/masTests/.swiftlint.yml @@ -9,3 +9,5 @@ disabled_rules: - force_cast - force_try - function_body_length +- implicitly_unwrapped_optional +- no_magic_numbers From c36a797ac2d5a092fd5971c22a6f9e9acc1021ec Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 21 Oct 2024 12:54:31 -0400 Subject: [PATCH 20/20] SwiftLint opt_in_rules: - all Resolve #592 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .swiftlint.yml | 141 ++++++---------------------------- Tests/masTests/.swiftlint.yml | 1 - 2 files changed, 25 insertions(+), 117 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 4897eef..3d19626 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -6,125 +6,34 @@ # --- opt_in_rules: -- accessibility_label_for_image -- accessibility_trait_for_button -- anonymous_argument_in_multiline_closure -- array_init -- attributes -- closure_end_indentation -- closure_spacing -- collection_alignment -- comma_inheritance -- conditional_returns_on_newline -- contains_over_filter_count -- contains_over_filter_is_empty -- contains_over_first_not_nil -- contains_over_range_nil_comparison -- convenience_type -- direct_return -- discarded_notification_center_observer -- discouraged_assert -- discouraged_none_name -- discouraged_object_literal -- discouraged_optional_boolean -- discouraged_optional_collection -- empty_collection_literal -- empty_count -- empty_string -- empty_xctest_method -- enum_case_associated_values_count -- expiring_todo -- explicit_init -- extension_access_modifier -- fallthrough -- fatal_error_message -- file_name_no_space -- file_types_order -- first_where -- flatmap_over_map_reduce -- function_default_parameter_at_end -- ibinspectable_in_extension -- identical_operands -- implicit_return -- implicitly_unwrapped_optional -- indentation_width -- joined_default_parameter -- last_where -- legacy_multiple -- let_var_whitespace -- literal_expression_end_indentation -- local_doc_comment -- lower_acl_than_parent -- missing_docs -- modifier_order -- multiline_arguments -- multiline_arguments_brackets -- multiline_function_chains -- multiline_literal_brackets -- multiline_parameters -- multiline_parameters_brackets -- nimble_operator -- no_empty_block -- no_extension_access_modifier -- no_magic_numbers -- non_overridable_class_declaration -- nslocalizedstring_key -- nslocalizedstring_require_bundle -- object_literal -- operator_usage_whitespace -- optional_enum_case_matching -- overridden_super_call -- override_in_extension -- pattern_matching_keywords -- period_spacing -- prefer_key_path -- prefer_self_in_static_references -- prefer_self_type_over_type_of_self -- prefer_zero_over_explicit_init -- private_action -- private_outlet -- private_subject -- private_swiftui_state -- prohibited_interface_builder -- prohibited_super_call -- quick_discouraged_focused_test -- raw_value_for_camel_cased_codable_enum -- reduce_into -- redundant_nil_coalescing -- redundant_self_in_closure -- redundant_type_annotation -- required_enum_case -- return_value_from_void_function -- self_binding -- shorthand_argument -- shorthand_optional_binding -- single_test_class -- sorted_first_last -- sorted_imports -- static_operator -- strict_fileprivate -- strong_iboutlet -- superfluous_else -- switch_case_on_newline -- test_case_accessibility -- toggle_bool -- trailing_closure -- type_contents_order -- unavailable_function -- unhandled_throwing_task -- unneeded_parentheses_in_closure_argument -- unowned_variable_capture -- untyped_error_in_catch -- unused_parameter -- vertical_parameter_alignment_on_call -- vertical_whitespace_closing_braces -- vertical_whitespace_opening_braces -- weak_delegate -- xct_specific_matcher -- yoda_condition +- all disabled_rules: +- balanced_xctest_lifecycle +- closure_body_length +- contrasted_opening_brace +- explicit_acl +- explicit_enum_raw_value +- explicit_top_level_acl +- explicit_type_interface +- file_header +- file_name +- final_test_case +- force_unwrapping - function_body_length +- inert_defer +- legacy_objc_type +- no_grouping_extension +- number_separator +- one_declaration_per_file +- prefer_nimble +- prefixed_toplevel_constant +- quick_discouraged_call +- quick_discouraged_pending_test +- required_deinit +- sorted_enum_cases - trailing_comma +- unused_capture_list +- vertical_whitespace_between_cases file_types_order: order: [ [main_type], diff --git a/Tests/masTests/.swiftlint.yml b/Tests/masTests/.swiftlint.yml index 409fabb..df984c3 100644 --- a/Tests/masTests/.swiftlint.yml +++ b/Tests/masTests/.swiftlint.yml @@ -8,6 +8,5 @@ disabled_rules: - force_cast - force_try -- function_body_length - implicitly_unwrapped_optional - no_magic_numbers