diff --git a/CHANGELOG.md b/CHANGELOG.md index fb33c57937..a5aeccc050 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,11 @@ [Cihat Gündüz](https://github.com/Dschee) [#2294](https://github.com/realm/SwiftLint/issues/2294) +* Add `nslocalizedstring_require_bundle` rule to ensure calls to + `NSLocalizedString` specify the bundle where the strings file is located. + [Matthew Healy](https://github.com/matthew-healy) + [#2595](https://github.com/realm/SwiftLint/2595) + #### Bug Fixes * `colon` rule now catches violations when declaring generic types with diff --git a/Rules.md b/Rules.md index 7d8259be05..2a8d0cedd3 100644 --- a/Rules.md +++ b/Rules.md @@ -95,6 +95,7 @@ * [No Grouping Extension](#no-grouping-extension) * [Notification Center Detachment](#notification-center-detachment) * [NSLocalizedString Key](#nslocalizedstring-key) +* [NSLocalizedString Require Bundle](#nslocalizedstring-require-bundle) * [NSObject Prefer isEqual](#nsobject-prefer-isequal) * [Number Separator](#number-separator) * [Object Literal](#object-literal) @@ -13549,6 +13550,60 @@ NSLocalizedString(↓"key_\(param)", comment: nil) +## NSLocalizedString Require Bundle + +Identifier | Enabled by default | Supports autocorrection | Kind | Analyzer | Minimum Swift Compiler Version +--- | --- | --- | --- | --- | --- +`nslocalizedstring_require_bundle` | Disabled | No | lint | No | 3.0.0 + +Calls to NSLocalisedString should specify the bundle which contains the strings file. + +### Examples + +
+Non Triggering Examples + +```swift +NSLocalizedString("someKey", bundle: .main, comment: "test") +``` + +```swift +NSLocalizedString("someKey", tableName: "a", + bundle: Bundle(for: A.self), + comment: "test") +``` + +```swift +NSLocalizedString("someKey", tableName: "xyz", + bundle: someBundle, value: "test" + comment: "test") +``` + +```swift +arbitraryFunctionCall("something") +``` + +
+
+Triggering Examples + +```swift +↓NSLocalizedString("someKey", comment: "test") +``` + +```swift +↓NSLocalizedString("someKey", tableName: "a", comment: "test") +``` + +```swift +↓NSLocalizedString("someKey", tableName: "xyz", + value: "test", comment: "test") +``` + +
+ + + ## NSObject Prefer isEqual Identifier | Enabled by default | Supports autocorrection | Kind | Analyzer | Minimum Swift Compiler Version diff --git a/Source/SwiftLintFramework/Models/MasterRuleList.swift b/Source/SwiftLintFramework/Models/MasterRuleList.swift index d3bdbe236a..b8ada707d2 100644 --- a/Source/SwiftLintFramework/Models/MasterRuleList.swift +++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift @@ -90,6 +90,7 @@ public let masterRuleList = RuleList(rules: [ MultilineParametersRule.self, MultipleClosuresWithTrailingClosureRule.self, NSLocalizedStringKeyRule.self, + NSLocalizedStringRequireBundleRule.self, NSObjectPreferIsEqualRule.self, NestingRule.self, NimbleOperatorRule.self, diff --git a/Source/SwiftLintFramework/Rules/Lint/NSLocalizedStringRequireBundleRule.swift b/Source/SwiftLintFramework/Rules/Lint/NSLocalizedStringRequireBundleRule.swift new file mode 100644 index 0000000000..2f5cca4b84 --- /dev/null +++ b/Source/SwiftLintFramework/Rules/Lint/NSLocalizedStringRequireBundleRule.swift @@ -0,0 +1,62 @@ +import SourceKittenFramework + +public struct NSLocalizedStringRequireBundleRule: ASTRule, OptInRule, ConfigurationProviderRule, AutomaticTestableRule { + public var configuration = SeverityConfiguration(.warning) + + public init() {} + + public static let description = RuleDescription( + identifier: "nslocalizedstring_require_bundle", + name: "NSLocalizedString Require Bundle", + description: "Calls to NSLocalisedString should specify the bundle which contains the strings file.", + kind: .lint, + nonTriggeringExamples: [ + """ + NSLocalizedString("someKey", bundle: .main, comment: "test") + """, + """ + NSLocalizedString("someKey", tableName: "a", + bundle: Bundle(for: A.self), + comment: "test") + """, + """ + NSLocalizedString("someKey", tableName: "xyz", + bundle: someBundle, value: "test" + comment: "test") + """, + """ + arbitraryFunctionCall("something") + """ + ], + triggeringExamples: [ + """ + ↓NSLocalizedString("someKey", comment: "test") + """, + """ + ↓NSLocalizedString("someKey", tableName: "a", comment: "test") + """, + """ + ↓NSLocalizedString("someKey", tableName: "xyz", + value: "test", comment: "test") + """ + ] + ) + + public func validate(file: File, + kind: SwiftExpressionKind, + dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] { + let isBundleArgument: ([String: SourceKitRepresentable]) -> Bool = { $0.name == "bundle" } + guard kind == .call, + dictionary.name == "NSLocalizedString", + let offset = dictionary.offset, + !dictionary.enclosedArguments.contains(where: isBundleArgument) else { + return [] + } + + return [ + StyleViolation(ruleDescription: type(of: self).description, + severity: configuration.severity, + location: Location(file: file, byteOffset: offset)) + ] + } +} diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index 35741043cb..66fb60dc23 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ 1F11B3CF1C252F23002E8FA8 /* ClosingBraceRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F11B3CE1C252F23002E8FA8 /* ClosingBraceRule.swift */; }; 24B4DF0D1D6DFDE90097803B /* RedundantNilCoalescingRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24B4DF0B1D6DFA370097803B /* RedundantNilCoalescingRule.swift */; }; 24E17F721B14BB3F008195BE /* File+Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E17F701B1481FF008195BE /* File+Cache.swift */; }; + 287F8B642230843000BDC504 /* NSLocalizedStringRequireBundleRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287F8B62223083ED00BDC504 /* NSLocalizedStringRequireBundleRule.swift */; }; 2882895F222975D00037CF5F /* NSObjectPreferIsEqualRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2882895B22287C9C0037CF5F /* NSObjectPreferIsEqualRule.swift */; }; 288289602229776C0037CF5F /* NSObjectPreferIsEqualRuleExamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2882895D22287E2C0037CF5F /* NSObjectPreferIsEqualRuleExamples.swift */; }; 29AD4C661F6EA1D5009B66E1 /* ContainsOverFirstNotNilRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29AD4C641F6EA16C009B66E1 /* ContainsOverFirstNotNilRule.swift */; }; @@ -500,6 +501,7 @@ 1F11B3CE1C252F23002E8FA8 /* ClosingBraceRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClosingBraceRule.swift; sourceTree = ""; }; 24B4DF0B1D6DFA370097803B /* RedundantNilCoalescingRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedundantNilCoalescingRule.swift; sourceTree = ""; }; 24E17F701B1481FF008195BE /* File+Cache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "File+Cache.swift"; sourceTree = ""; }; + 287F8B62223083ED00BDC504 /* NSLocalizedStringRequireBundleRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLocalizedStringRequireBundleRule.swift; sourceTree = ""; }; 2882895B22287C9C0037CF5F /* NSObjectPreferIsEqualRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSObjectPreferIsEqualRule.swift; sourceTree = ""; }; 2882895D22287E2C0037CF5F /* NSObjectPreferIsEqualRuleExamples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSObjectPreferIsEqualRuleExamples.swift; sourceTree = ""; }; 29AD4C641F6EA16C009B66E1 /* ContainsOverFirstNotNilRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainsOverFirstNotNilRule.swift; sourceTree = ""; }; @@ -1083,6 +1085,7 @@ D4DABFD61E2C23B1009617B6 /* NotificationCenterDetachmentRule.swift */, D4DABFD81E2C59BC009617B6 /* NotificationCenterDetachmentRuleExamples.swift */, D41985E621F85014003BE2B7 /* NSLocalizedStringKeyRule.swift */, + 287F8B62223083ED00BDC504 /* NSLocalizedStringRequireBundleRule.swift */, 2882895B22287C9C0037CF5F /* NSObjectPreferIsEqualRule.swift */, 2882895D22287E2C0037CF5F /* NSObjectPreferIsEqualRuleExamples.swift */, 78F032441D7C877800BE709A /* OverriddenSuperCallRule.swift */, @@ -1939,6 +1942,7 @@ D4FBADD01E00DA0400669C73 /* OperatorUsageWhitespaceRule.swift in Sources */, D4C4A3521DEFBBB700E0E04C /* FileHeaderConfiguration.swift in Sources */, 623675B01F960C5C009BE6F3 /* QuickDiscouragedPendingTestRule.swift in Sources */, + 287F8B642230843000BDC504 /* NSLocalizedStringRequireBundleRule.swift in Sources */, D47079AD1DFE2FA700027086 /* EmptyParametersRule.swift in Sources */, E87E4A091BFB9CAE00FCFE46 /* SyntaxKind+SwiftLint.swift in Sources */, 3B0B14541C505D6300BE82F7 /* SeverityConfiguration.swift in Sources */, diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 1ef4bb6ba4..16d12e2e46 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -826,6 +826,12 @@ extension NSLocalizedStringKeyRuleTests { ] } +extension NSLocalizedStringRequireBundleRuleTests { + static var allTests: [(String, (NSLocalizedStringRequireBundleRuleTests) -> () throws -> Void)] = [ + ("testWithDefaultConfiguration", testWithDefaultConfiguration) + ] +} + extension NSObjectPreferIsEqualRuleTests { static var allTests: [(String, (NSObjectPreferIsEqualRuleTests) -> () throws -> Void)] = [ ("testWithDefaultConfiguration", testWithDefaultConfiguration) @@ -1590,6 +1596,7 @@ XCTMain([ testCase(MultilineParametersRuleTests.allTests), testCase(MultipleClosuresWithTrailingClosureRuleTests.allTests), testCase(NSLocalizedStringKeyRuleTests.allTests), + testCase(NSLocalizedStringRequireBundleRuleTests.allTests), testCase(NSObjectPreferIsEqualRuleTests.allTests), testCase(NestingRuleTests.allTests), testCase(NimbleOperatorRuleTests.allTests), diff --git a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift index d2ab101b03..159531ebf3 100644 --- a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift +++ b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift @@ -384,6 +384,12 @@ class NSLocalizedStringKeyRuleTests: XCTestCase { } } +class NSLocalizedStringRequireBundleRuleTests: XCTestCase { + func testWithDefaultConfiguration() { + verifyRule(NSLocalizedStringRequireBundleRule.description) + } +} + class NSObjectPreferIsEqualRuleTests: XCTestCase { func testWithDefaultConfiguration() { verifyRule(NSObjectPreferIsEqualRule.description)