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)