From 356eaa5c3d1ad62d16a766f0f5b9f69388b736b3 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Mon, 25 May 2026 16:00:04 +1000 Subject: [PATCH 1/3] Enable and address `redundant_nil_coalescing` SwiftLint rule --- .swiftlint.yml | 3 +++ .../Helpers/ImageSaliencyService.swift | 2 +- .../WordPressKit/DomainsServiceRemote.swift | 4 ++-- .../Sources/WordPressKit/JetpackScanThreat.swift | 2 +- .../WordPressKitModels/RemoteUser+Likes.swift | 2 +- Sources/WordPressData/Swift/SiteTaxonomy.swift | 14 +++++++------- .../RegisterDomainDetailsViewModelTests.swift | 4 ++-- .../Tests/MediaLibraryTestSupport.swift | 2 +- WordPress/Classes/Stores/StatsPeriodStore.swift | 2 +- .../Jetpack Scan/JetpackScanCoordinator.swift | 2 +- .../Detail/Views/ReaderDetailHeaderView.swift | 2 +- .../Reader/Manage/OffsetTableViewHandler.swift | 4 ++-- .../Support/SupportTableViewController.swift | 2 +- 13 files changed, 24 insertions(+), 21 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index f262105f33f1..c6ea793c0348 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -100,6 +100,9 @@ only_rules: # Prefer `reduce(into:_:)` over `reduce(_:_:)` for non-trivial accumulator types. - reduce_into + # `nil` coalescing on a non-optional is redundant. + - redundant_nil_coalescing + # Type annotations are redundant when the type can be inferred. - redundant_type_annotation diff --git a/Modules/Sources/AsyncImageKit/Helpers/ImageSaliencyService.swift b/Modules/Sources/AsyncImageKit/Helpers/ImageSaliencyService.swift index 8469cafcae38..143d36ac4b5b 100644 --- a/Modules/Sources/AsyncImageKit/Helpers/ImageSaliencyService.swift +++ b/Modules/Sources/AsyncImageKit/Helpers/ImageSaliencyService.swift @@ -137,7 +137,7 @@ private final class SaliencyCache: @unchecked Sendable { } func cachedRect(for url: URL) -> CGRect? { - lock.withLock { store[url.absoluteString] ?? nil } + lock.withLock { store[url.absoluteString]} } func store(_ rect: CGRect?, for url: URL) { diff --git a/Modules/Sources/WordPressKit/DomainsServiceRemote.swift b/Modules/Sources/WordPressKit/DomainsServiceRemote.swift index 0284f22e4171..08c291a1b82c 100644 --- a/Modules/Sources/WordPressKit/DomainsServiceRemote.swift +++ b/Modules/Sources/WordPressKit/DomainsServiceRemote.swift @@ -59,8 +59,8 @@ public struct DomainSuggestion: Codable { } self.domainName = domain - self.productID = json["product_id"] as? Int ?? nil - self.supportsPrivacy = json["supports_privacy"] as? Bool ?? nil + self.productID = json["product_id"] as? Int + self.supportsPrivacy = json["supports_privacy"] as? Bool self.costString = json["cost"] as? String ?? "" self.cost = json["raw_price"] as? Double self.saleCost = json["sale_cost"] as? Double diff --git a/Modules/Sources/WordPressKit/JetpackScanThreat.swift b/Modules/Sources/WordPressKit/JetpackScanThreat.swift index fdf5076154d8..af41fda284b8 100644 --- a/Modules/Sources/WordPressKit/JetpackScanThreat.swift +++ b/Modules/Sources/WordPressKit/JetpackScanThreat.swift @@ -96,7 +96,7 @@ public struct JetpackScanThreat: Decodable { description = try container.decode(String.self, forKey: .description) firstDetected = try container.decode(Date.self, forKey: .firstDetected) fixedOn = try container.decodeIfPresent(Date.self, forKey: .fixedOn) - fixable = try? container.decodeIfPresent(JetpackScanThreatFixer.self, forKey: .fixable) ?? nil + fixable = try? container.decodeIfPresent(JetpackScanThreatFixer.self, forKey: .fixable) `extension` = try container.decodeIfPresent(JetpackThreatExtension.self, forKey: .extension) diff = try container.decodeIfPresent(String.self, forKey: .diff) rows = try container.decodeIfPresent([String: Any].self, forKey: .rows) diff --git a/Modules/Sources/WordPressKitModels/RemoteUser+Likes.swift b/Modules/Sources/WordPressKitModels/RemoteUser+Likes.swift index aeb51f5991fa..a04a3c42956d 100644 --- a/Modules/Sources/WordPressKitModels/RemoteUser+Likes.swift +++ b/Modules/Sources/WordPressKitModels/RemoteUser+Likes.swift @@ -49,7 +49,7 @@ import Foundation public init(dictionary: [String: Any]) { blogUrl = dictionary["url"] as? String ?? "" blogName = dictionary["name"] as? String ?? "" - blogID = dictionary["id"] as? NSNumber ?? nil + blogID = dictionary["id"] as? NSNumber iconUrl = { if let iconInfo = dictionary["icon"] as? [String: Any], diff --git a/Sources/WordPressData/Swift/SiteTaxonomy.swift b/Sources/WordPressData/Swift/SiteTaxonomy.swift index 6b52b32c0f1b..77f3f643707a 100644 --- a/Sources/WordPressData/Swift/SiteTaxonomy.swift +++ b/Sources/WordPressData/Swift/SiteTaxonomy.swift @@ -33,13 +33,13 @@ public struct SiteTaxonomy: Codable { self.name = details.name self.restBase = details.restBase self.labels = LocalizedLabels( - name: details.labels[.name] ?? nil, - newItemName: details.labels[.newItemName] ?? nil, - addNewItem: details.labels[.addNewItem] ?? nil, - nameFieldDescription: details.labels[.nameFieldDescription] ?? nil, - descFieldDescription: details.labels[.descFieldDescription] ?? nil, - noTerms: details.labels[.noTerms] ?? nil, - searchItems: details.labels[.searchItems] ?? nil + name: details.labels[.name], + newItemName: details.labels[.newItemName], + addNewItem: details.labels[.addNewItem], + nameFieldDescription: details.labels[.nameFieldDescription], + descFieldDescription: details.labels[.descFieldDescription], + noTerms: details.labels[.noTerms], + searchItems: details.labels[.searchItems] ) self.supportedPostTypes = details.types } diff --git a/Tests/KeystoneTests/Tests/Features/Domains/RegisterDomainDetailsViewModelTests.swift b/Tests/KeystoneTests/Tests/Features/Domains/RegisterDomainDetailsViewModelTests.swift index 02a8265161e1..fe627c863620 100644 --- a/Tests/KeystoneTests/Tests/Features/Domains/RegisterDomainDetailsViewModelTests.swift +++ b/Tests/KeystoneTests/Tests/Features/Domains/RegisterDomainDetailsViewModelTests.swift @@ -8,8 +8,8 @@ extension FullyQuotedDomainSuggestion { } let domainName = domain - let productID = json["product_id"] as? Int ?? nil - let supportsPrivacy = json["supports_privacy"] as? Bool ?? nil + let productID = json["product_id"] as? Int + let supportsPrivacy = json["supports_privacy"] as? Bool let costString = json["cost"] as? String ?? "" let saleCostString: String? = nil diff --git a/Tests/WordPressKitTests/WordPressKitTests/Tests/MediaLibraryTestSupport.swift b/Tests/WordPressKitTests/WordPressKitTests/Tests/MediaLibraryTestSupport.swift index 2714668e8b2d..6a30406721b4 100644 --- a/Tests/WordPressKitTests/WordPressKitTests/Tests/MediaLibraryTestSupport.swift +++ b/Tests/WordPressKitTests/WordPressKitTests/Tests/MediaLibraryTestSupport.swift @@ -58,7 +58,7 @@ extension MediaLibraryTestSupport { } private func handleREST(request: URLRequest, failAtPage pageToFail: Int) -> HTTPStubsResponse { - let cursor = request.url?.query("page_handle") ?? nil + let cursor = request.url?.query("page_handle") let number = request.url?.query("number").flatMap(Int.init(_:)) ?? 100 let cursorIndex = media.firstIndex { $0.cusor == cursor } ?? 0 diff --git a/WordPress/Classes/Stores/StatsPeriodStore.swift b/WordPress/Classes/Stores/StatsPeriodStore.swift index 2bc18795c756..25f673272bb3 100644 --- a/WordPress/Classes/Stores/StatsPeriodStore.swift +++ b/WordPress/Classes/Stores/StatsPeriodStore.swift @@ -1160,7 +1160,7 @@ extension StatsPeriodStore { guard let postId else { return nil } - return state.postStats[postId] ?? nil + return state.postStats[postId] } func getMostRecentDate(forPost postId: Int?) -> Date? { diff --git a/WordPress/Classes/ViewRelated/Jetpack/Jetpack Scan/JetpackScanCoordinator.swift b/WordPress/Classes/ViewRelated/Jetpack/Jetpack Scan/JetpackScanCoordinator.swift index a92158fc88a7..cb7b595f6885 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Jetpack Scan/JetpackScanCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Jetpack Scan/JetpackScanCoordinator.swift @@ -46,7 +46,7 @@ class JetpackScanCoordinator { let returnThreats: [JetpackScanThreat]? if scan?.state == .fixingThreats { - returnThreats = scan?.threatFixStatus?.compactMap { $0.threat } ?? nil + returnThreats = scan?.threatFixStatus?.compactMap { $0.threat } } else { returnThreats = scan?.state == .idle ? scan?.threats : nil } diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift index 518364ac381b..c7709a3349de 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailHeaderView.swift @@ -140,7 +140,7 @@ class ReaderDetailHeaderViewModel: ObservableObject { self.isFollowingSite = post.isFollowing - self.authorAvatarURL = post.avatarURLForDisplay() ?? nil + self.authorAvatarURL = post.avatarURLForDisplay() if let authorName = post.authorForDisplay(), !authorName.isEmpty { self.authorName = authorName diff --git a/WordPress/Classes/ViewRelated/Reader/Manage/OffsetTableViewHandler.swift b/WordPress/Classes/ViewRelated/Reader/Manage/OffsetTableViewHandler.swift index 8c00fc9de655..26175a6ac1f3 100644 --- a/WordPress/Classes/ViewRelated/Reader/Manage/OffsetTableViewHandler.swift +++ b/WordPress/Classes/ViewRelated/Reader/Manage/OffsetTableViewHandler.swift @@ -35,11 +35,11 @@ class OffsetTableViewHandler: WPTableViewHandler { let oldIndexPath = indexPath.map { adjustedToTable(indexPath: $0) - } ?? nil + } let newPath = newIndexPath.map { adjustedToTable(indexPath: $0) - } ?? nil + } super.controller(controller, didChange: anObject, at: oldIndexPath, for: type, newIndexPath: newPath) } diff --git a/WordPress/Classes/ViewRelated/Support/SupportTableViewController.swift b/WordPress/Classes/ViewRelated/Support/SupportTableViewController.swift index e8d661f39502..8f0b549d5cd4 100644 --- a/WordPress/Classes/ViewRelated/Support/SupportTableViewController.swift +++ b/WordPress/Classes/ViewRelated/Support/SupportTableViewController.swift @@ -540,7 +540,7 @@ private extension SupportTableViewController { // MARK: - Helpers func controllerToShowFrom() -> UIViewController? { - return showHelpFromViewController ?? navigationController ?? nil + return showHelpFromViewController ?? navigationController } // MARK: - Localized Text From 09850ba3223953d2c3a64780514b72430aad3499 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 26 May 2026 19:58:25 +1000 Subject: [PATCH 2/3] Restore essential `?? nil` flattens The `redundant_nil_coalescing` rule enabled in 356eaa5c3d treats `?? nil` as syntactically redundant regardless of whether the LHS is `T?` (truly redundant) or `T??` (essential flatten). Four sites were flattening `T??` and stopped compiling once the rule autofix removed them. Switch each to a form the rule doesn't trip: - `Dictionary[key, default: nil]` on `[K: V?]` returns `V?` directly, avoiding the `T??` intermediate from a raw subscript. - A `do`/`catch` around `decodeIfPresent` in `JetpackScanThreat` preserves "treat malformed value as absent" semantics without producing the `T??` that `try?` over a throwing `T?` would. `bundle exec fastlane ios test` passes (3415 tests, 0 failures). --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Helpers/ImageSaliencyService.swift | 2 +- .../Sources/WordPressKit/JetpackScanThreat.swift | 6 +++++- Sources/WordPressData/Swift/SiteTaxonomy.swift | 14 +++++++------- WordPress/Classes/Stores/StatsPeriodStore.swift | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Modules/Sources/AsyncImageKit/Helpers/ImageSaliencyService.swift b/Modules/Sources/AsyncImageKit/Helpers/ImageSaliencyService.swift index 143d36ac4b5b..1371c14e7add 100644 --- a/Modules/Sources/AsyncImageKit/Helpers/ImageSaliencyService.swift +++ b/Modules/Sources/AsyncImageKit/Helpers/ImageSaliencyService.swift @@ -137,7 +137,7 @@ private final class SaliencyCache: @unchecked Sendable { } func cachedRect(for url: URL) -> CGRect? { - lock.withLock { store[url.absoluteString]} + lock.withLock { store[url.absoluteString, default: nil] } } func store(_ rect: CGRect?, for url: URL) { diff --git a/Modules/Sources/WordPressKit/JetpackScanThreat.swift b/Modules/Sources/WordPressKit/JetpackScanThreat.swift index af41fda284b8..1e3378ffd83b 100644 --- a/Modules/Sources/WordPressKit/JetpackScanThreat.swift +++ b/Modules/Sources/WordPressKit/JetpackScanThreat.swift @@ -96,7 +96,11 @@ public struct JetpackScanThreat: Decodable { description = try container.decode(String.self, forKey: .description) firstDetected = try container.decode(Date.self, forKey: .firstDetected) fixedOn = try container.decodeIfPresent(Date.self, forKey: .fixedOn) - fixable = try? container.decodeIfPresent(JetpackScanThreatFixer.self, forKey: .fixable) + do { + fixable = try container.decodeIfPresent(JetpackScanThreatFixer.self, forKey: .fixable) + } catch { + fixable = nil + } `extension` = try container.decodeIfPresent(JetpackThreatExtension.self, forKey: .extension) diff = try container.decodeIfPresent(String.self, forKey: .diff) rows = try container.decodeIfPresent([String: Any].self, forKey: .rows) diff --git a/Sources/WordPressData/Swift/SiteTaxonomy.swift b/Sources/WordPressData/Swift/SiteTaxonomy.swift index 77f3f643707a..065ee3b4c8f5 100644 --- a/Sources/WordPressData/Swift/SiteTaxonomy.swift +++ b/Sources/WordPressData/Swift/SiteTaxonomy.swift @@ -33,13 +33,13 @@ public struct SiteTaxonomy: Codable { self.name = details.name self.restBase = details.restBase self.labels = LocalizedLabels( - name: details.labels[.name], - newItemName: details.labels[.newItemName], - addNewItem: details.labels[.addNewItem], - nameFieldDescription: details.labels[.nameFieldDescription], - descFieldDescription: details.labels[.descFieldDescription], - noTerms: details.labels[.noTerms], - searchItems: details.labels[.searchItems] + name: details.labels[.name, default: nil], + newItemName: details.labels[.newItemName, default: nil], + addNewItem: details.labels[.addNewItem, default: nil], + nameFieldDescription: details.labels[.nameFieldDescription, default: nil], + descFieldDescription: details.labels[.descFieldDescription, default: nil], + noTerms: details.labels[.noTerms, default: nil], + searchItems: details.labels[.searchItems, default: nil] ) self.supportedPostTypes = details.types } diff --git a/WordPress/Classes/Stores/StatsPeriodStore.swift b/WordPress/Classes/Stores/StatsPeriodStore.swift index 25f673272bb3..b9a1f69f3ac5 100644 --- a/WordPress/Classes/Stores/StatsPeriodStore.swift +++ b/WordPress/Classes/Stores/StatsPeriodStore.swift @@ -1160,7 +1160,7 @@ extension StatsPeriodStore { guard let postId else { return nil } - return state.postStats[postId] + return state.postStats[postId, default: nil] } func getMostRecentDate(forPost postId: Int?) -> Date? { From 19cb3e2f3a3b38f91900d9e925c2570c85f5c220 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Fri, 29 May 2026 13:23:58 +1000 Subject: [PATCH 3/3] Simplify `fixable` decode to `try?` over `do-catch` Per review feedback: dropping `?? nil` from the original `try?` expression is enough to satisfy the SwiftLint rule. The `do-catch` was an equivalent but verbose respelling. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 --- Modules/Sources/WordPressKit/JetpackScanThreat.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Modules/Sources/WordPressKit/JetpackScanThreat.swift b/Modules/Sources/WordPressKit/JetpackScanThreat.swift index 1e3378ffd83b..af41fda284b8 100644 --- a/Modules/Sources/WordPressKit/JetpackScanThreat.swift +++ b/Modules/Sources/WordPressKit/JetpackScanThreat.swift @@ -96,11 +96,7 @@ public struct JetpackScanThreat: Decodable { description = try container.decode(String.self, forKey: .description) firstDetected = try container.decode(Date.self, forKey: .firstDetected) fixedOn = try container.decodeIfPresent(Date.self, forKey: .fixedOn) - do { - fixable = try container.decodeIfPresent(JetpackScanThreatFixer.self, forKey: .fixable) - } catch { - fixable = nil - } + fixable = try? container.decodeIfPresent(JetpackScanThreatFixer.self, forKey: .fixable) `extension` = try container.decodeIfPresent(JetpackThreatExtension.self, forKey: .extension) diff = try container.decodeIfPresent(String.self, forKey: .diff) rows = try container.decodeIfPresent([String: Any].self, forKey: .rows)