From 3086b8dacb4bd1bfd76b474e9311b2b6cdcbbd24 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 5 May 2026 08:11:00 +1000 Subject: [PATCH] Enable SwiftLint rule: first_where Adds the rule to `only_rules`. The rule prefers `xs.first(where: { ... })` over `xs.filter { ... }.first`, which is a performance and clarity win. Part of the Orchard SwiftLint rollout campaign. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Code Opus 4.7 --- .swiftlint.yml | 3 +++ .../FormattableContent/FormattableContent.swift | 4 ++-- Modules/Sources/WordPressKit/PlanServiceRemote.swift | 2 +- .../Tests/Extensions/URLHelpersTests.swift | 2 +- .../Features/Notifications/NotificationUtility.swift | 2 +- .../Services/NotificationSettingsServiceTests.swift | 12 ++++++------ WordPress/Classes/Services/JetpackScanService.swift | 2 +- .../Migration/MigrationDeepLinkRouter.swift | 2 +- .../ViewModel/RegisterDomainDetailsViewModel.swift | 8 ++++---- .../Processors/GutenbergGalleryUploadProcessor.swift | 4 ++-- .../Processors/GutenbergImgUploadProcessor.swift | 4 ++-- .../Sources/Services/AppExtensionsService.swift | 2 +- .../UI/ShareCategoriesPickerViewController.swift | 2 +- 13 files changed, 26 insertions(+), 23 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 3a07daa9eb53..d699403ae173 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -42,6 +42,9 @@ only_rules: # Prefer checking `isEmpty` over comparing to empty string literal `""`. - empty_string + # Prefer `first(where:)` over `filter { }.first`. + - first_where + # When calling the `joined` method on an array of strings, omit an # explicit empty-string separator since that is the default. - joined_default_parameter diff --git a/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContent.swift b/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContent.swift index 6d09ac5b3171..7b59bc61a5cd 100644 --- a/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContent.swift +++ b/Modules/Sources/FormattableContentKit/FormattableContent/FormattableContent.swift @@ -29,9 +29,9 @@ extension FormattableContent { } public func action(id: Identifier) -> FormattableContentAction? { - return actions?.filter { + return actions?.first(where: { $0.identifier == id - }.first + }) } public func range(with url: URL) -> FormattableContentRange? { diff --git a/Modules/Sources/WordPressKit/PlanServiceRemote.swift b/Modules/Sources/WordPressKit/PlanServiceRemote.swift index 4f8000c21f0a..798e51db8f49 100644 --- a/Modules/Sources/WordPressKit/PlanServiceRemote.swift +++ b/Modules/Sources/WordPressKit/PlanServiceRemote.swift @@ -201,7 +201,7 @@ open class PlanServiceRemote: ServiceRemoteWordPressComREST { await wordPressComRestApi.perform(.get, URLString: path, parameters: parameters, type: ZendeskSiteContainer.self) .eraseToError() .flatMap { container in - guard let metadata = container.body.sites.filter({ $0.ID == siteID }).first?.zendeskMetadata else { + guard let metadata = container.body.sites.first(where: { $0.ID == siteID })?.zendeskMetadata else { return .failure(PlanServiceRemoteError.noMetadata) } return .success(metadata) diff --git a/Tests/KeystoneTests/Tests/Extensions/URLHelpersTests.swift b/Tests/KeystoneTests/Tests/Extensions/URLHelpersTests.swift index e702e74e510c..9000eaa22fa1 100644 --- a/Tests/KeystoneTests/Tests/Extensions/URLHelpersTests.swift +++ b/Tests/KeystoneTests/Tests/Extensions/URLHelpersTests.swift @@ -17,7 +17,7 @@ class URLHelpersTests: XCTestCase { XCTAssertNotEqual(url.absoluteString, newURL.absoluteString) let components = URLComponents(url: newURL, resolvingAgainstBaseURL: false) - let cacheBusterQueryItem = components?.queryItems?.filter { $0.name == "_" }.first + let cacheBusterQueryItem = components?.queryItems?.first(where: { $0.name == "_" }) XCTAssertNotNil(cacheBusterQueryItem?.value) } } diff --git a/Tests/KeystoneTests/Tests/Features/Notifications/NotificationUtility.swift b/Tests/KeystoneTests/Tests/Features/Notifications/NotificationUtility.swift index bae10c3a107b..3f41f5e81b61 100644 --- a/Tests/KeystoneTests/Tests/Features/Notifications/NotificationUtility.swift +++ b/Tests/KeystoneTests/Tests/Features/Notifications/NotificationUtility.swift @@ -51,7 +51,7 @@ class NotificationUtility { let dictionary = try JSONObject(fromFileNamed: "notifications-replied-comment.json") let body = dictionary["body"] let blocks = NotificationContentFactory.content(from: body as! [[String: AnyObject]], actionsParser: NotificationActionParser(), parent: WordPressData.Notification(context: context)) - return blocks.filter { $0.kind == .comment }.first! as! FormattableCommentContent + return blocks.first(where: { $0.kind == .comment })! as! FormattableCommentContent } func mockCommentContext() throws -> ActionContext { diff --git a/Tests/KeystoneTests/Tests/Services/NotificationSettingsServiceTests.swift b/Tests/KeystoneTests/Tests/Services/NotificationSettingsServiceTests.swift index 43a272f1fbc1..b23ad039e568 100644 --- a/Tests/KeystoneTests/Tests/Services/NotificationSettingsServiceTests.swift +++ b/Tests/KeystoneTests/Tests/Services/NotificationSettingsServiceTests.swift @@ -52,9 +52,9 @@ class NotificationSettingsServiceTests: CoreDataTestCase { let targetSite = targetSettings.first! XCTAssert(targetSite.streams.count == 3, "Error while parsing Site Stream Settings") - let parsedDeviceSettings = targetSite.streams.filter { $0.kind == StreamKind.device }.first - let parsedEmailSettings = targetSite.streams.filter { $0.kind == StreamKind.email }.first - let parsedTimelineSettings = targetSite.streams.filter { $0.kind == StreamKind.timeline }.first + let parsedDeviceSettings = targetSite.streams.first(where: { $0.kind == StreamKind.device }) + let parsedEmailSettings = targetSite.streams.first(where: { $0.kind == StreamKind.email }) + let parsedTimelineSettings = targetSite.streams.first(where: { $0.kind == StreamKind.timeline }) let expectedTimelineSettings = [ "new_comment": false, @@ -103,9 +103,9 @@ class NotificationSettingsServiceTests: CoreDataTestCase { let otherSettings = filteredSettings.first! XCTAssert(otherSettings.streams.count == 3, "Error while parsing Other Streams") - let parsedDeviceSettings = otherSettings.streams.filter { $0.kind == StreamKind.device }.first - let parsedEmailSettings = otherSettings.streams.filter { $0.kind == StreamKind.email }.first - let parsedTimelineSettings = otherSettings.streams.filter { $0.kind == StreamKind.timeline }.first + let parsedDeviceSettings = otherSettings.streams.first(where: { $0.kind == StreamKind.device }) + let parsedEmailSettings = otherSettings.streams.first(where: { $0.kind == StreamKind.email }) + let parsedTimelineSettings = otherSettings.streams.first(where: { $0.kind == StreamKind.timeline }) let expectedDeviceSettings = [ "comment_like": true, diff --git a/WordPress/Classes/Services/JetpackScanService.swift b/WordPress/Classes/Services/JetpackScanService.swift index a07216fac438..47972c966013 100644 --- a/WordPress/Classes/Services/JetpackScanService.swift +++ b/WordPress/Classes/Services/JetpackScanService.swift @@ -71,7 +71,7 @@ class JetpackScanService { continue } - var threat = threats.filter({ $0.id == item.threatId }).first + var threat = threats.first(where: { $0.id == item.threatId }) if item.status == .inProgress { threat?.status = .fixing } diff --git a/WordPress/Classes/Utility/Universal Links/Migration/MigrationDeepLinkRouter.swift b/WordPress/Classes/Utility/Universal Links/Migration/MigrationDeepLinkRouter.swift index e46d281e0a15..b40b504a62f4 100644 --- a/WordPress/Classes/Utility/Universal Links/Migration/MigrationDeepLinkRouter.swift +++ b/WordPress/Classes/Utility/Universal Links/Migration/MigrationDeepLinkRouter.swift @@ -46,7 +46,7 @@ struct MigrationDeepLinkRouter: LinkRouter { func handle(url: URL, shouldTrack track: Bool = false, source: DeepLinkSource? = nil) { guard let deepLinkPath = url.host, - let route = routes.filter({ $0.path.removingPrefix("/") == deepLinkPath }).first else { + let route = routes.first(where: { $0.path.removingPrefix("/") == deepLinkPath }) else { return } diff --git a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewModel/RegisterDomainDetailsViewModel.swift b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewModel/RegisterDomainDetailsViewModel.swift index 108d2281ba18..faea7839b0ec 100644 --- a/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewModel/RegisterDomainDetailsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Domains/Domain registration/RegisterDomainDetails/ViewModel/RegisterDomainDetailsViewModel.swift @@ -372,9 +372,9 @@ class RegisterDomainDetailsViewModel { section.rows[safe: addressSectionIndexHelper.postalCodeIndex]?.editableRow?.value = domainContactInformation.postalCode section.rows[safe: addressSectionIndexHelper.addressLine1]?.editableRow?.value = domainContactInformation.address1 section.rows[safe: addressSectionIndexHelper.stateIndex]?.editableRow?.idValue = domainContactInformation.state - section.rows[safe: addressSectionIndexHelper.stateIndex]?.editableRow?.value = states?.filter { + section.rows[safe: addressSectionIndexHelper.stateIndex]?.editableRow?.value = states?.first(where: { return $0.code == domainContactInformation.state - }.first?.name + })?.name } private func updatePhoneSection(with domainContactInformation: DomainContactInformation) { @@ -391,9 +391,9 @@ class RegisterDomainDetailsViewModel { private func updateContactInformationSection(with domainContactInformation: DomainContactInformation) { let section = sections[SectionIndex.contactInformation.rawValue] section.rows[safe: CellIndex.ContactInformation.country.rawValue]?.editableRow?.idValue = domainContactInformation.countryCode - section.rows[safe: CellIndex.ContactInformation.country.rawValue]?.editableRow?.value = countries?.filter { + section.rows[safe: CellIndex.ContactInformation.country.rawValue]?.editableRow?.value = countries?.first(where: { return $0.code == domainContactInformation.countryCode - }.first?.name + })?.name section.rows[safe: CellIndex.ContactInformation.email.rawValue]?.editableRow?.value = domainContactInformation.email section.rows[safe: CellIndex.ContactInformation.firstName.rawValue]?.editableRow?.value = domainContactInformation.firstName section.rows[safe: CellIndex.ContactInformation.lastName.rawValue]?.editableRow?.value = domainContactInformation.lastName diff --git a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergGalleryUploadProcessor.swift b/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergGalleryUploadProcessor.swift index 71afaf0cdadc..1bff0fbe726c 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergGalleryUploadProcessor.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergGalleryUploadProcessor.swift @@ -39,9 +39,9 @@ class GutenbergGalleryUploadProcessor: GutenbergProcessor { let classAttributes = imgClass.components(separatedBy: " ") - guard let imageIDAttribute = classAttributes.filter({ (value) -> Bool in + guard let imageIDAttribute = classAttributes.first(where: { (value) -> Bool in value.hasPrefix(GutenbergImgUploadProcessor.imgClassIDPrefixAttribute) - }).first else { + }) else { return } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergImgUploadProcessor.swift b/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergImgUploadProcessor.swift index 857a738f2962..0da1cba80baa 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergImgUploadProcessor.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/Processors/GutenbergImgUploadProcessor.swift @@ -21,9 +21,9 @@ class GutenbergImgUploadProcessor: GutenbergProcessor { } let classAttributes = imgClass.components(separatedBy: " ") - guard let imageIDAttribute = classAttributes.filter({ (value) -> Bool in + guard let imageIDAttribute = classAttributes.first(where: { (value) -> Bool in value.hasPrefix(GutenbergImgUploadProcessor.imgClassIDPrefixAttribute) - }).first else { + }) else { return } diff --git a/WordPress/WordPressShareExtension/Sources/Services/AppExtensionsService.swift b/WordPress/WordPressShareExtension/Sources/Services/AppExtensionsService.swift index ea86a0d63760..6cd125d6a506 100644 --- a/WordPress/WordPressShareExtension/Sources/Services/AppExtensionsService.swift +++ b/WordPress/WordPressShareExtension/Sources/Services/AppExtensionsService.swift @@ -127,7 +127,7 @@ extension AppExtensionsService { /// func isAuthorizedToUploadMedia(in sites: [RemoteBlog], for selectedSiteID: Int) -> Bool { let siteID = NSNumber(value: selectedSiteID) - guard let isAuthorizedToUploadFiles = sites.filter({$0.blogID == siteID}).first?.isUploadingFilesAllowed() else { + guard let isAuthorizedToUploadFiles = sites.first(where: {$0.blogID == siteID})?.isUploadingFilesAllowed() else { return false } diff --git a/WordPress/WordPressShareExtension/Sources/UI/ShareCategoriesPickerViewController.swift b/WordPress/WordPressShareExtension/Sources/UI/ShareCategoriesPickerViewController.swift index 1bb3ae29eb11..c0336ae00639 100644 --- a/WordPress/WordPressShareExtension/Sources/UI/ShareCategoriesPickerViewController.swift +++ b/WordPress/WordPressShareExtension/Sources/UI/ShareCategoriesPickerViewController.swift @@ -82,7 +82,7 @@ class ShareCategoriesPickerViewController: UITableViewController { // Add the default site to the selected list if it's empty. var selected = categoryInfo.selectedCategories ?? [] - if selected.isEmpty, let defaultCategory = categoryInfo.allCategories?.filter({$0.categoryID == categoryInfo.defaultCategoryID }).first { + if selected.isEmpty, let defaultCategory = categoryInfo.allCategories?.first(where: {$0.categoryID == categoryInfo.defaultCategoryID }) { selected.append(defaultCategory) } self.selectedCategories = selected