You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/Scripts/LintLocalizableStrings.swift

621 lines
28 KiB
Swift

#!/usr/bin/xcrun --sdk macosx swift
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
//
// This script is based on https://github.com/ginowu7/CleanSwiftLocalizableExample the main difference
// is canges to the localized usage regex
//
// stringlint:disable
import Foundation
extension ProjectState {
/// Adding `// stringlint:disable` to the top of a source file (before imports) or after a string will mean that file/line gets
/// ignored by this script (good for some things like the auto-generated emoji strings or debug strings)
static let lintSuppression: String = "stringlint:disable"
static let primaryLocalisationFile: String = "en"
static let validLocalisationSuffixes: Set<String> = ["Localizable.strings"]
static let validSourceSuffixes: Set<String> = [".swift", ".m"]
static let excludedPaths: Set<String> = [
"build/", // Files under the build folder (CI)
"Pods/", // The pods folder
"Protos/", // The protobuf files
".xcassets/", // Asset bundles
".app/", // App build directories
".appex/", // Extension build directories
"tests/", // Exclude test directories
"_SharedTestUtilities/", // Exclude shared test directory
"external/" // External dependencies
]
static let excludedPhrases: Set<String> = [ "", " ", ",", ", ", ".", "/", "\\n", "null" ]
static let excludedUnlocalisedStringLineMatching: Set<MatchType> = [
.contains(ProjectState.lintSuppression, caseSensitive: false),
.prefix("#import", caseSensitive: false),
.prefix("@available(", caseSensitive: false),
.contains("fatalError(", caseSensitive: false),
.contains("precondition(", caseSensitive: false),
.contains("preconditionFailure(", caseSensitive: false),
.contains("print(", caseSensitive: false),
.contains("SNLog(", caseSensitive: false),
.contains("Log.setup(", caseSensitive: false),
Merge remote-tracking branch 'origin/feature/swift-package-manager' into feature/groups-rebuild # Conflicts: # Podfile # Podfile.lock # Session.xcodeproj/project.pbxproj # Session/Calls/Call Management/SessionCall.swift # Session/Calls/Call Management/SessionCallManager.swift # Session/Calls/CallVC.swift # Session/Conversations/ConversationVC+Interaction.swift # Session/Conversations/ConversationVC.swift # Session/Conversations/ConversationViewModel.swift # Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift # Session/Conversations/Settings/ThreadSettingsViewModel.swift # Session/Emoji/Emoji+Available.swift # Session/Home/GlobalSearch/GlobalSearchViewController.swift # Session/Home/HomeVC.swift # Session/Home/HomeViewModel.swift # Session/Home/New Conversation/NewDMVC.swift # Session/Media Viewing & Editing/DocumentTitleViewController.swift # Session/Media Viewing & Editing/GIFs/GifPickerCell.swift # Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift # Session/Media Viewing & Editing/ImagePickerController.swift # Session/Media Viewing & Editing/MediaTileViewController.swift # Session/Media Viewing & Editing/PhotoCapture.swift # Session/Media Viewing & Editing/PhotoCaptureViewController.swift # Session/Media Viewing & Editing/PhotoLibrary.swift # Session/Media Viewing & Editing/SendMediaNavigationController.swift # Session/Meta/AppDelegate.swift # Session/Meta/AppEnvironment.swift # Session/Meta/MainAppContext.swift # Session/Meta/SessionApp.swift # Session/Notifications/NotificationPresenter.swift # Session/Notifications/PushRegistrationManager.swift # Session/Notifications/SyncPushTokensJob.swift # Session/Notifications/UserNotificationsAdaptee.swift # Session/Onboarding/LandingVC.swift # Session/Onboarding/LinkDeviceVC.swift # Session/Onboarding/Onboarding.swift # Session/Onboarding/RegisterVC.swift # Session/Onboarding/RestoreVC.swift # Session/Settings/HelpViewModel.swift # Session/Settings/NukeDataModal.swift # Session/Shared/FullConversationCell.swift # Session/Shared/OWSBezierPathView.m # Session/Utilities/BackgroundPoller.swift # Session/Utilities/MockDataGenerator.swift # SessionMessagingKit/Configuration.swift # SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift # SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift # SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift # SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift # SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift # SessionMessagingKit/Database/Models/Attachment.swift # SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift # SessionMessagingKit/Database/Models/Interaction.swift # SessionMessagingKit/Database/Models/Profile.swift # SessionMessagingKit/Database/Models/SessionThread.swift # SessionMessagingKit/File Server/FileServerAPI.swift # SessionMessagingKit/Jobs/AttachmentDownloadJob.swift # SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift # SessionMessagingKit/Jobs/DisappearingMessagesJob.swift # SessionMessagingKit/Jobs/FailedMessageSendsJob.swift # SessionMessagingKit/Jobs/MessageSendJob.swift # SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift # SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift # SessionMessagingKit/Messages/Message.swift # SessionMessagingKit/Open Groups/Crypto/Crypto+OpenGroupAPI.swift # SessionMessagingKit/Open Groups/Models/SOGSMessage.swift # SessionMessagingKit/Open Groups/OpenGroupAPI.swift # SessionMessagingKit/Open Groups/OpenGroupManager.swift # SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ExpirationTimers.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+LegacyClosedGroups.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver.swift # SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift # SessionMessagingKit/Sending & Receiving/MessageSender.swift # SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift # SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift # SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift # SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupAPI+Poller.swift # SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift # SessionMessagingKit/Utilities/ProfileManager.swift # SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift # SessionMessagingKitTests/LibSession/LibSessionSpec.swift # SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift # SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift # SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift # SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift # SessionMessagingKitTests/Utilities/CryptoSMKSpec.swift # SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift # SessionNotificationServiceExtension/NSENotificationPresenter.swift # SessionNotificationServiceExtension/NotificationServiceExtension.swift # SessionShareExtension/ShareAppExtensionContext.swift # SessionShareExtension/ShareNavController.swift # SessionShareExtension/ThreadPickerVC.swift # SessionSnodeKit/Crypto/Crypto+SessionSnodeKit.swift # SessionSnodeKit/Models/DeleteAllBeforeResponse.swift # SessionSnodeKit/Models/DeleteAllMessagesResponse.swift # SessionSnodeKit/Models/DeleteMessagesResponse.swift # SessionSnodeKit/Models/RevokeSubkeyRequest.swift # SessionSnodeKit/Models/RevokeSubkeyResponse.swift # SessionSnodeKit/Models/SendMessageResponse.swift # SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift # SessionSnodeKit/Models/UpdateExpiryAllResponse.swift # SessionSnodeKit/Models/UpdateExpiryResponse.swift # SessionSnodeKit/Networking/SnodeAPI.swift # SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift # SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift # SessionTests/Database/DatabaseSpec.swift # SessionTests/Settings/NotificationContentViewModelSpec.swift # SessionUIKit/Components/ToastController.swift # SessionUIKit/Style Guide/Values.swift # SessionUtilitiesKit/Crypto/Crypto+SessionUtilitiesKit.swift # SessionUtilitiesKit/Crypto/Crypto.swift # SessionUtilitiesKit/Database/Models/Identity.swift # SessionUtilitiesKit/Database/Models/Job.swift # SessionUtilitiesKit/Database/Storage.swift # SessionUtilitiesKit/Database/Types/Migration.swift # SessionUtilitiesKit/General/AppContext.swift # SessionUtilitiesKit/General/Data+Utilities.swift # SessionUtilitiesKit/General/Logging.swift # SessionUtilitiesKit/General/SNUserDefaults.swift # SessionUtilitiesKit/General/String+Trimming.swift # SessionUtilitiesKit/General/String+Utilities.swift # SessionUtilitiesKit/General/TimestampUtils.swift # SessionUtilitiesKit/General/UIEdgeInsets.swift # SessionUtilitiesKit/JobRunner/JobRunner.swift # SessionUtilitiesKit/LibSession/LibSessionError.swift # SessionUtilitiesKit/Media/DataSource.swift # SessionUtilitiesKit/Meta/SessionUtilitiesKit.h # SessionUtilitiesKit/Networking/NetworkType.swift # SessionUtilitiesKit/Networking/ProxiedContentDownloader.swift # SessionUtilitiesKit/Utilities/BackgroundTaskManager.swift # SessionUtilitiesKit/Utilities/BencodeResponse.swift # SessionUtilitiesKit/Utilities/CExceptionHelper.mm # SessionUtilitiesKit/Utilities/FileManagerType.swift # SessionUtilitiesKit/Utilities/KeychainStorageType.swift # SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift # SessionUtilitiesKitTests/Database/Utilities/PersistableRecordUtilitiesSpec.swift # SessionUtilitiesKitTests/General/SessionIdSpec.swift # SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift # SignalUtilitiesKit/Configuration.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalInputAccessoryView.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift # SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCropViewController.swift # SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift # SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift # SignalUtilitiesKit/Meta/SignalUtilitiesKit.h # SignalUtilitiesKit/Shared View Controllers/OWSViewController.swift # SignalUtilitiesKit/Shared Views/CircleView.swift # SignalUtilitiesKit/Shared Views/TappableView.swift # SignalUtilitiesKit/Utilities/AppSetup.swift # SignalUtilitiesKit/Utilities/Bench.swift # SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift # _SharedTestUtilities/CommonMockedExtensions.swift # _SharedTestUtilities/MockCrypto.swift # _SharedTestUtilities/Mocked.swift # _SharedTestUtilities/SynchronousStorage.swift
10 months ago
.containsAnd(
"primaryPrefix:",
caseSensitive: false,
.previousLine(numEarlier: 1, .contains("Log.setup(with: Logger(", caseSensitive: false))
),
.containsAnd(
"customDirectory:",
caseSensitive: false,
.previousLine(numEarlier: 2, .contains("Log.setup(with: Logger(", caseSensitive: false))
),
.contains("Log.verbose(", caseSensitive: false),
.contains("Log.debug(", caseSensitive: false),
.contains("Log.info(", caseSensitive: false),
.contains("Log.warn(", caseSensitive: false),
.contains("Log.error(", caseSensitive: false),
.contains("Log.critical(", caseSensitive: false),
.contains("logMessage:", caseSensitive: false),
.contains("owsFailDebug(", caseSensitive: false),
Merge remote-tracking branch 'origin/feature/swift-package-manager' into feature/groups-rebuild # Conflicts: # Podfile # Podfile.lock # Session.xcodeproj/project.pbxproj # Session/Calls/Call Management/SessionCall.swift # Session/Calls/Call Management/SessionCallManager.swift # Session/Calls/CallVC.swift # Session/Conversations/ConversationVC+Interaction.swift # Session/Conversations/ConversationVC.swift # Session/Conversations/ConversationViewModel.swift # Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift # Session/Conversations/Settings/ThreadSettingsViewModel.swift # Session/Emoji/Emoji+Available.swift # Session/Home/GlobalSearch/GlobalSearchViewController.swift # Session/Home/HomeVC.swift # Session/Home/HomeViewModel.swift # Session/Home/New Conversation/NewDMVC.swift # Session/Media Viewing & Editing/DocumentTitleViewController.swift # Session/Media Viewing & Editing/GIFs/GifPickerCell.swift # Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift # Session/Media Viewing & Editing/ImagePickerController.swift # Session/Media Viewing & Editing/MediaTileViewController.swift # Session/Media Viewing & Editing/PhotoCapture.swift # Session/Media Viewing & Editing/PhotoCaptureViewController.swift # Session/Media Viewing & Editing/PhotoLibrary.swift # Session/Media Viewing & Editing/SendMediaNavigationController.swift # Session/Meta/AppDelegate.swift # Session/Meta/AppEnvironment.swift # Session/Meta/MainAppContext.swift # Session/Meta/SessionApp.swift # Session/Notifications/NotificationPresenter.swift # Session/Notifications/PushRegistrationManager.swift # Session/Notifications/SyncPushTokensJob.swift # Session/Notifications/UserNotificationsAdaptee.swift # Session/Onboarding/LandingVC.swift # Session/Onboarding/LinkDeviceVC.swift # Session/Onboarding/Onboarding.swift # Session/Onboarding/RegisterVC.swift # Session/Onboarding/RestoreVC.swift # Session/Settings/HelpViewModel.swift # Session/Settings/NukeDataModal.swift # Session/Shared/FullConversationCell.swift # Session/Shared/OWSBezierPathView.m # Session/Utilities/BackgroundPoller.swift # Session/Utilities/MockDataGenerator.swift # SessionMessagingKit/Configuration.swift # SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift # SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift # SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift # SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift # SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift # SessionMessagingKit/Database/Models/Attachment.swift # SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift # SessionMessagingKit/Database/Models/Interaction.swift # SessionMessagingKit/Database/Models/Profile.swift # SessionMessagingKit/Database/Models/SessionThread.swift # SessionMessagingKit/File Server/FileServerAPI.swift # SessionMessagingKit/Jobs/AttachmentDownloadJob.swift # SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift # SessionMessagingKit/Jobs/DisappearingMessagesJob.swift # SessionMessagingKit/Jobs/FailedMessageSendsJob.swift # SessionMessagingKit/Jobs/MessageSendJob.swift # SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift # SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift # SessionMessagingKit/Messages/Message.swift # SessionMessagingKit/Open Groups/Crypto/Crypto+OpenGroupAPI.swift # SessionMessagingKit/Open Groups/Models/SOGSMessage.swift # SessionMessagingKit/Open Groups/OpenGroupAPI.swift # SessionMessagingKit/Open Groups/OpenGroupManager.swift # SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ExpirationTimers.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+LegacyClosedGroups.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver.swift # SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift # SessionMessagingKit/Sending & Receiving/MessageSender.swift # SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift # SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift # SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift # SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupAPI+Poller.swift # SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift # SessionMessagingKit/Utilities/ProfileManager.swift # SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift # SessionMessagingKitTests/LibSession/LibSessionSpec.swift # SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift # SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift # SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift # SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift # SessionMessagingKitTests/Utilities/CryptoSMKSpec.swift # SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift # SessionNotificationServiceExtension/NSENotificationPresenter.swift # SessionNotificationServiceExtension/NotificationServiceExtension.swift # SessionShareExtension/ShareAppExtensionContext.swift # SessionShareExtension/ShareNavController.swift # SessionShareExtension/ThreadPickerVC.swift # SessionSnodeKit/Crypto/Crypto+SessionSnodeKit.swift # SessionSnodeKit/Models/DeleteAllBeforeResponse.swift # SessionSnodeKit/Models/DeleteAllMessagesResponse.swift # SessionSnodeKit/Models/DeleteMessagesResponse.swift # SessionSnodeKit/Models/RevokeSubkeyRequest.swift # SessionSnodeKit/Models/RevokeSubkeyResponse.swift # SessionSnodeKit/Models/SendMessageResponse.swift # SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift # SessionSnodeKit/Models/UpdateExpiryAllResponse.swift # SessionSnodeKit/Models/UpdateExpiryResponse.swift # SessionSnodeKit/Networking/SnodeAPI.swift # SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift # SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift # SessionTests/Database/DatabaseSpec.swift # SessionTests/Settings/NotificationContentViewModelSpec.swift # SessionUIKit/Components/ToastController.swift # SessionUIKit/Style Guide/Values.swift # SessionUtilitiesKit/Crypto/Crypto+SessionUtilitiesKit.swift # SessionUtilitiesKit/Crypto/Crypto.swift # SessionUtilitiesKit/Database/Models/Identity.swift # SessionUtilitiesKit/Database/Models/Job.swift # SessionUtilitiesKit/Database/Storage.swift # SessionUtilitiesKit/Database/Types/Migration.swift # SessionUtilitiesKit/General/AppContext.swift # SessionUtilitiesKit/General/Data+Utilities.swift # SessionUtilitiesKit/General/Logging.swift # SessionUtilitiesKit/General/SNUserDefaults.swift # SessionUtilitiesKit/General/String+Trimming.swift # SessionUtilitiesKit/General/String+Utilities.swift # SessionUtilitiesKit/General/TimestampUtils.swift # SessionUtilitiesKit/General/UIEdgeInsets.swift # SessionUtilitiesKit/JobRunner/JobRunner.swift # SessionUtilitiesKit/LibSession/LibSessionError.swift # SessionUtilitiesKit/Media/DataSource.swift # SessionUtilitiesKit/Meta/SessionUtilitiesKit.h # SessionUtilitiesKit/Networking/NetworkType.swift # SessionUtilitiesKit/Networking/ProxiedContentDownloader.swift # SessionUtilitiesKit/Utilities/BackgroundTaskManager.swift # SessionUtilitiesKit/Utilities/BencodeResponse.swift # SessionUtilitiesKit/Utilities/CExceptionHelper.mm # SessionUtilitiesKit/Utilities/FileManagerType.swift # SessionUtilitiesKit/Utilities/KeychainStorageType.swift # SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift # SessionUtilitiesKitTests/Database/Utilities/PersistableRecordUtilitiesSpec.swift # SessionUtilitiesKitTests/General/SessionIdSpec.swift # SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift # SignalUtilitiesKit/Configuration.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalInputAccessoryView.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift # SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCropViewController.swift # SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift # SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift # SignalUtilitiesKit/Meta/SignalUtilitiesKit.h # SignalUtilitiesKit/Shared View Controllers/OWSViewController.swift # SignalUtilitiesKit/Shared Views/CircleView.swift # SignalUtilitiesKit/Shared Views/TappableView.swift # SignalUtilitiesKit/Utilities/AppSetup.swift # SignalUtilitiesKit/Utilities/Bench.swift # SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift # _SharedTestUtilities/CommonMockedExtensions.swift # _SharedTestUtilities/MockCrypto.swift # _SharedTestUtilities/Mocked.swift # _SharedTestUtilities/SynchronousStorage.swift
10 months ago
.contains("error: .other(", caseSensitive: false),
.contains("#imageLiteral(resourceName:", caseSensitive: false),
.contains("UIImage(named:", caseSensitive: false),
.contains("UIImage(systemName:", caseSensitive: false),
.contains("[UIImage imageNamed:", caseSensitive: false),
.contains("UIFont(name:", caseSensitive: false),
.contains(".dateFormat =", caseSensitive: false),
.contains(".accessibilityLabel =", caseSensitive: false),
.contains(".accessibilityValue =", caseSensitive: false),
.contains(".accessibilityIdentifier =", caseSensitive: false),
.contains("accessibilityIdentifier:", caseSensitive: false),
.contains("accessibilityLabel:", caseSensitive: false),
.contains("Accessibility(identifier:", caseSensitive: false),
.contains("Accessibility(label:", caseSensitive: false),
.contains("NSAttributedString.Key(", caseSensitive: false),
.contains("Notification.Name(", caseSensitive: false),
.contains("Notification.Key(", caseSensitive: false),
.contains("DispatchQueue(", caseSensitive: false),
.containsAnd(
"identifier:",
caseSensitive: false,
.previousLine(numEarlier: 1, .contains("Accessibility(", caseSensitive: false))
),
.containsAnd(
"label:",
caseSensitive: false,
.previousLine(numEarlier: 1, .contains("Accessibility(", caseSensitive: false))
),
.containsAnd(
"label:",
caseSensitive: false,
.previousLine(numEarlier: 2, .contains("Accessibility(", caseSensitive: false))
),
.contains("SQL(", caseSensitive: false),
.regex(".*static var databaseTableName: String"),
.regex("case .* = "),
.regex("Error.*\\("),
.regex("Crypto.*\\(id:"),
.containsAnd("id:", caseSensitive: false, .previousLine(numEarlier: 1, .regex("Crypto.*\\("))),
.regex(".*\\.like\\(\".*%\""),
.containsAnd(
"identifier:",
caseSensitive: false,
.previousLine(numEarlier: 1, .contains("Dependencies.create", caseSensitive: false))
)
]
}
// Execute the desired actions
let targetActions: Set<ScriptAction> = {
let args = CommandLine.arguments
// The first argument is the file name
guard args.count > 1 else { return [.lintStrings] }
return Set(args.suffix(from: 1).map { (ScriptAction(rawValue: $0) ?? .lintStrings) })
}()
print("------------ Searching Through Files ------------")
let projectState: ProjectState = ProjectState(
path: (
ProcessInfo.processInfo.environment["PROJECT_DIR"] ??
FileManager.default.currentDirectoryPath
),
loadSourceFiles: targetActions.contains(.lintStrings)
)
2 years ago
print("------------ Processing \(projectState.localizationFiles.count) Localization File(s) ------------")
targetActions.forEach { $0.perform(projectState: projectState) }
// MARK: - ScriptAction
enum ScriptAction: String {
case validateFilesCopied = "validate"
case lintStrings = "lint"
func perform(projectState: ProjectState) {
// Perform the action
switch self {
case .validateFilesCopied:
print("------------ Checking Copied Files ------------")
guard
let builtProductsPath: String = ProcessInfo.processInfo.environment["BUILT_PRODUCTS_DIR"],
let productName: String = ProcessInfo.processInfo.environment["FULL_PRODUCT_NAME"],
let productPathInfo = try? URL(fileURLWithPath: "\(builtProductsPath)/\(productName)")
.resourceValues(forKeys: [.isSymbolicLinkKey, .isAliasFileKey]),
let finalProductUrl: URL = try? { () -> URL in
let possibleAliasUrl: URL = URL(fileURLWithPath: "\(builtProductsPath)/\(productName)")
guard productPathInfo.isSymbolicLink == true || productPathInfo.isAliasFile == true else {
return possibleAliasUrl
}
return try URL(resolvingAliasFileAt: possibleAliasUrl, options: URL.BookmarkResolutionOptions())
}(),
let enumerator: FileManager.DirectoryEnumerator = FileManager.default.enumerator(
at: finalProductUrl,
includingPropertiesForKeys: [.isDirectoryKey],
options: [.skipsHiddenFiles]
),
let fileUrls: [URL] = enumerator.allObjects as? [URL]
else { return Output.error("Could not retrieve list of files within built product") }
let localizationFiles: Set<String> = Set(fileUrls
.filter { $0.path.hasSuffix(".lproj") }
.map { $0.lastPathComponent.replacingOccurrences(of: ".lproj", with: "") })
let missingFiles: Set<String> = Set(projectState.localizationFiles
.map { $0.name })
.subtracting(localizationFiles)
guard missingFiles.isEmpty else {
return Output.error("Translations missing from \(productName): \(missingFiles.joined(separator: ", "))")
}
break
case .lintStrings:
guard !projectState.localizationFiles.isEmpty else {
return print("------------ Nothing to lint ------------")
}
// Add warnings for any duplicate keys
projectState.localizationFiles.forEach { file in
// Show errors for any duplicates
file.duplicates.forEach { phrase, original in Output.duplicate(phrase, original: original) }
// Show warnings for any phrases missing from the file
let allKeys: Set<String> = Set(file.keyPhrase.keys)
let missingKeysFromOtherFiles: [String: [String]] = projectState.localizationFiles.reduce(into: [:]) { result, otherFile in
guard otherFile.path != file.path else { return }
let missingKeys: Set<String> = Set(otherFile.keyPhrase.keys)
.subtracting(allKeys)
missingKeys.forEach { missingKey in
result[missingKey] = ((result[missingKey] ?? []) + [otherFile.name])
}
}
missingKeysFromOtherFiles.forEach { missingKey, namesOfFilesItWasFound in
Output.warning(file, "Phrase '\(missingKey)' is missing (found in: \(namesOfFilesItWasFound.joined(separator: ", ")))")
}
}
// Process the source code
print("------------ Processing \(projectState.sourceFiles.count) Source File(s) ------------")
let allKeys: Set<String> = Set(projectState.primaryLocalizationFile.keyPhrase.keys)
projectState.sourceFiles.forEach { file in
// Add logs for unlocalised strings
file.unlocalizedPhrases.forEach { phrase in
Output.warning(phrase, "Found unlocalized string '\(phrase.key)'")
}
// Add errors for missing localised strings
let missingKeys: Set<String> = Set(file.keyPhrase.keys).subtracting(allKeys)
missingKeys.forEach { key in
switch file.keyPhrase[key] {
case .some(let phrase): Output.error(phrase, "Localized phrase '\(key)' missing from strings files")
case .none: Output.error(file, "Localized phrase '\(key)' missing from strings files")
}
}
}
break
}
print("------------ Complete ------------")
}
}
// MARK: - Functionality
enum Regex {
/// Returns a list of strings that match regex pattern from content
///
/// - Parameters:
/// - pattern: regex pattern
/// - content: content to match
/// - Returns: list of results
static func matches(_ pattern: String, content: String) -> [String] {
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
fatalError("Regex not formatted correctly: \(pattern)")
}
let matches = regex.matches(in: content, options: [], range: NSRange(location: 0, length: content.utf16.count))
return matches.map {
guard let range = Range($0.range(at: 0), in: content) else {
fatalError("Incorrect range match")
}
return String(content[range])
}
}
}
// MARK: - Output
enum Output {
static func error(_ error: String) {
print("error: \(error)")
}
static func error(_ location: Locatable, _ error: String) {
print("\(location.location): error: \(error)")
}
static func warning(_ location: Locatable, _ warning: String) {
print("\(location.location): warning: \(warning)")
}
static func duplicate(
_ duplicate: KeyedLocatable,
original: KeyedLocatable
) {
print("\(duplicate.location): error: duplicate key '\(original.key)'")
// Looks like the `note:` doesn't work the same as when XCode does it unfortunately so we can't
// currently include the reference to the original entry
// print("\(original.location): note: previously found here")
}
}
// MARK: - ProjectState
struct ProjectState {
let primaryLocalizationFile: LocalizationStringsFile
let localizationFiles: [LocalizationStringsFile]
let sourceFiles: [SourceFile]
init(path: String, loadSourceFiles: Bool) {
guard
let enumerator: FileManager.DirectoryEnumerator = FileManager.default.enumerator(
at: URL(fileURLWithPath: path),
includingPropertiesForKeys: [.isDirectoryKey],
options: [.skipsHiddenFiles]
),
let fileUrls: [URL] = enumerator.allObjects as? [URL]
else { fatalError("Could not locate files in path directory: \(path)") }
// Get a list of valid URLs
let lowerCaseExcludedPaths: Set<String> = Set(ProjectState.excludedPaths.map { $0.lowercased() })
let validFileUrls: [URL] = fileUrls.filter { fileUrl in
((try? fileUrl.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == false) &&
!lowerCaseExcludedPaths.contains { fileUrl.path.lowercased().contains($0) }
}
// Localization files
let targetFileSuffixes: Set<String> = Set(ProjectState.validLocalisationSuffixes.map { $0.lowercased() })
self.localizationFiles = validFileUrls
.filter { fileUrl in targetFileSuffixes.contains { fileUrl.path.lowercased().contains($0) } }
.map { LocalizationStringsFile(path: $0.path) }
guard let primaryLocalizationFile: LocalizationStringsFile = self.localizationFiles.first(where: { $0.name == ProjectState.primaryLocalisationFile }) else {
fatalError("Could not locate primary localization file: \(ProjectState.primaryLocalisationFile)")
}
self.primaryLocalizationFile = primaryLocalizationFile
guard loadSourceFiles else {
self.sourceFiles = []
return
}
// Source files
let lowerCaseSourceSuffixes: Set<String> = Set(ProjectState.validSourceSuffixes.map { $0.lowercased() })
self.sourceFiles = validFileUrls
.filter { fileUrl in lowerCaseSourceSuffixes.contains(".\(fileUrl.pathExtension)") }
.compactMap { SourceFile(path: $0.path) }
}
}
protocol Locatable {
var location: String { get }
}
protocol KeyedLocatable: Locatable {
var key: String { get }
}
extension ProjectState {
// MARK: - LocalizationStringsFile
struct LocalizationStringsFile: Locatable {
struct Phrase: KeyedLocatable {
let key: String
let value: String
let filePath: String
let lineNumber: Int
var location: String { "\(filePath):\(lineNumber)" }
}
let name: String
let path: String
let keyPhrase: [String: Phrase]
let duplicates: [(Phrase, original: Phrase)]
var location: String { path }
init(path: String) {
let result = LocalizationStringsFile.parse(path)
self.name = (path
.replacingOccurrences(of: "/Localizable.strings", with: "")
.replacingOccurrences(of: ".lproj", with: "")
.components(separatedBy: "/")
.last ?? "Unknown")
self.path = path
self.keyPhrase = result.keyPhrase
self.duplicates = result.duplicates
}
static func parse(_ path: String) -> (keyPhrase: [String: Phrase], duplicates: [(Phrase, original: Phrase)]) {
guard
let data: Data = FileManager.default.contents(atPath: path),
let content: String = String(data: data, encoding: .utf8)
else { fatalError("Could not read from path: \(path)") }
let lines: [String] = content.components(separatedBy: .newlines)
var duplicates: [(Phrase, original: Phrase)] = []
var keyPhrase: [String: Phrase] = [:]
lines.enumerated().forEach { lineNumber, line in
guard
let key: String = Regex.matches("\"([^\"]*?)\"(?= =)", content: line).first,
let value: String = Regex.matches("(?<== )\"(.*?)\"(?=;)", content: line).first
else { return }
// Remove the quotation marks around the key
let trimmedKey: String = String(key
.prefix(upTo: key.index(before: key.endIndex))
.suffix(from: key.index(after: key.startIndex)))
// Files are 1-indexed but arrays are 0-indexed so add 1 to the lineNumber
let result: Phrase = Phrase(
key: trimmedKey,
value: value,
filePath: path,
lineNumber: (lineNumber + 1)
)
switch keyPhrase[trimmedKey] {
case .some(let original): duplicates.append((result, original))
case .none: keyPhrase[trimmedKey] = result
}
}
return (keyPhrase, duplicates)
}
}
// MARK: - SourceFile
struct SourceFile: Locatable {
struct Phrase: KeyedLocatable {
let term: String
let filePath: String
let lineNumber: Int
var key: String { term }
var location: String { "\(filePath):\(lineNumber)" }
}
let path: String
let keyPhrase: [String: Phrase]
let unlocalizedKeyPhrase: [String: Phrase]
let phrases: [Phrase]
let unlocalizedPhrases: [Phrase]
var location: String { path }
init?(path: String) {
guard let result = SourceFile.parse(path) else { return nil }
self.path = path
self.keyPhrase = result.keyPhrase
self.unlocalizedKeyPhrase = result.unlocalizedKeyPhrase
self.phrases = result.phrases
self.unlocalizedPhrases = result.unlocalizedPhrases
}
static func parse(_ path: String) -> (keyPhrase: [String: Phrase], phrases: [Phrase], unlocalizedKeyPhrase: [String: Phrase], unlocalizedPhrases: [Phrase])? {
guard
let data: Data = FileManager.default.contents(atPath: path),
let content: String = String(data: data, encoding: .utf8)
else { fatalError("Could not read from path: \(path)") }
// If the file has the lint supression before the first import then ignore the file
let preImportContent: String = (content.components(separatedBy: "import").first ?? "")
guard !preImportContent.contains(ProjectState.lintSuppression) else {
print("Explicitly ignoring \(path)")
return nil
}
// Otherwise continue and process the file
let lines: [String] = content.components(separatedBy: .newlines)
var keyPhrase: [String: Phrase] = [:]
var unlocalizedKeyPhrase: [String: Phrase] = [:]
var phrases: [Phrase] = []
var unlocalizedPhrases: [Phrase] = []
lines.enumerated().forEach { lineNumber, line in
let trimmedLine: String = line.trimmingCharacters(in: .whitespacesAndNewlines)
// Ignore the line if it doesn't contain a quotation character (optimisation), it's
// been suppressed or it's explicitly excluded due to the rules at the top of the file
guard
trimmedLine.contains("\"") &&
!ProjectState.excludedUnlocalisedStringLineMatching
.contains(where: { $0.matches(trimmedLine, lineNumber, lines) })
else { return }
// Split line based on commented out content and exclude the comment from the linting
let commentMatches: [String] = Regex.matches(
"//[^\\\"]*(?:\\\"[^\\\"]*\\\"[^\\\"]*)*",
content: line
)
let targetLine: String = (commentMatches.isEmpty ? line :
line.components(separatedBy: commentMatches[0])[0]
)
// Use regex to find `NSLocalizedString("", "")`, `"".localised()` and any other `""`
// values in the source code
//
// Note: It's more complex because we need to exclude escaped quotation marks from
// strings and also want to ignore any strings that have been commented out, Swift
// also doesn't support "lookbehind" in regex so we can use that approach
var isUnlocalized: Bool = false
var allMatches: Set<String> = Set(
Regex
.matches(
"NSLocalizedString\\(@{0,1}\\\"[^\\\"\\\\]*(?:\\\\.[^\\\"\\\\]*)*(?:\\\")",
content: targetLine
)
.map { match in
match
.removingPrefixIfPresent("NSLocalizedString(@\"")
.removingPrefixIfPresent("NSLocalizedString(\"")
.removingSuffixIfPresent("\")")
.removingSuffixIfPresent("\"")
}
)
// If we didn't get any matches for the standard `NSLocalizedString` then try our
// custom extension `"".localized()`
if allMatches.isEmpty {
allMatches = allMatches.union(Set(
Regex
.matches(
"\\\"[^\\\"\\\\]*(?:\\\\.[^\\\"\\\\]*)*\\\"\\.localized",
content: targetLine
)
.map { match in
match
.removingPrefixIfPresent("\"")
.removingSuffixIfPresent("\".localized")
}
))
}
/// If we still don't have any matches then try to match any strings as unlocalized strings (handling
/// nested `"Test\"string\" value"`, empty strings and strings only composed of quotes `"""""""`)
///
/// **Note:** While it'd be nice to have the regex automatically exclude the quotes doing so makes it _far_ less
/// efficient (approx. by a factor of 8 times) so we remove those ourselves)
if allMatches.isEmpty {
// Find strings which are just not localised
let potentialUnlocalizedStrings: [String] = Regex
.matches("\\\"[^\\\"\\\\]*(?:\\\\.[^\\\"\\\\]*)*(?:\\\")", content: targetLine)
// Remove the leading and trailing quotation marks
.map { $0.removingPrefixIfPresent("\"").removingSuffixIfPresent("\"") }
// Remove any empty strings
.filter { !$0.isEmpty }
// Remove any string conversations (ie. `.map { "\($0)" }`
.filter { value in !value.hasPrefix("\\(") || !value.hasSuffix(")") }
allMatches = allMatches.union(Set(potentialUnlocalizedStrings))
isUnlocalized = true
}
// Remove any excluded phrases from the matches
allMatches = allMatches.subtracting(ProjectState.excludedPhrases.map { "\($0)" })
allMatches.forEach { match in
// Files are 1-indexed but arrays are 0-indexed so add 1 to the lineNumber
let result: Phrase = Phrase(
term: match,
filePath: path,
lineNumber: (lineNumber + 1)
)
if !isUnlocalized {
keyPhrase[match] = result
phrases.append(result)
}
else {
unlocalizedKeyPhrase[match] = result
unlocalizedPhrases.append(result)
}
}
}
return (keyPhrase, phrases, unlocalizedKeyPhrase, unlocalizedPhrases)
}
}
}
indirect enum MatchType: Hashable {
case prefix(String, caseSensitive: Bool)
case contains(String, caseSensitive: Bool)
case containsAnd(String, caseSensitive: Bool, MatchType)
case regex(String)
case previousLine(numEarlier: Int, MatchType)
func matches(_ value: String, _ index: Int, _ lines: [String]) -> Bool {
switch self {
case .prefix(let prefix, false):
return value
.lowercased()
.trimmingCharacters(in: .whitespacesAndNewlines)
.hasPrefix(prefix.lowercased())
case .prefix(let prefix, true):
return value
.trimmingCharacters(in: .whitespacesAndNewlines)
.hasPrefix(prefix)
case .contains(let other, false): return value.lowercased().contains(other.lowercased())
case .contains(let other, true): return value.contains(other)
case .containsAnd(let other, false, let otherMatch):
guard value.lowercased().contains(other.lowercased()) else { return false }
return otherMatch.matches(value, index, lines)
case .containsAnd(let other, true, let otherMatch):
guard value.contains(other) else { return false }
return otherMatch.matches(value, index, lines)
case .regex(let regex): return !Regex.matches(regex, content: value).isEmpty
case .previousLine(let numEarlier, let type):
guard index >= numEarlier else { return false }
let targetIndex: Int = (index - numEarlier)
return type.matches(lines[targetIndex], targetIndex, lines)
}
}
}
extension String {
func removingPrefixIfPresent(_ value: String) -> String {
guard hasPrefix(value) else { return self }
return String(self.suffix(from: self.index(self.startIndex, offsetBy: value.count)))
}
func removingSuffixIfPresent(_ value: String) -> String {
guard hasSuffix(value) else { return self }
return String(self.prefix(upTo: self.index(self.endIndex, offsetBy: -value.count)))
}
}