diff --git a/CHANGELOG.md b/CHANGELOG.md
index e105e87c5ef..1deaf57df65 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -31,6 +31,11 @@
[Ryan Booker](https://github.com/ryanbooker)
[#1761](https://github.com/realm/SwiftLint/issues/1761)
+* Add `quick_single_spec` opt-in rule to enforce test files
+ to contain a single QuickSpec classes.
+ [Ornithologist Coder](https://github.com/ornithocoder)
+ [#1779](https://github.com/realm/SwiftLint/issues/1779)
+
##### Bug Fixes
* Fix false positive on `force_unwrapping` rule when declaring
diff --git a/Rules.md b/Rules.md
index 9c894713602..2c981a82e58 100644
--- a/Rules.md
+++ b/Rules.md
@@ -67,6 +67,7 @@
* [Private Unit Test](#private-unit-test)
* [Prohibited calls to super](#prohibited-calls-to-super)
* [Protocol Property Accessors Order](#protocol-property-accessors-order)
+* [Quick Single Spec](#quick-single-spec)
* [Redundant Discardable Let](#redundant-discardable-let)
* [Redundant Nil Coalescing](#redundant-nil-coalescing)
* [Redundant Optional Initialization](#redundant-optional-initialization)
@@ -7906,6 +7907,50 @@ protocol Foo {
+## Quick Single Spec
+
+Identifier | Enabled by default | Supports autocorrection | Kind
+--- | --- | --- | ---
+`quick_single_spec` | Disabled | No | style
+
+Test files should contain a single QuickSpec class.
+
+### Examples
+
+
+Non Triggering Examples
+
+```swift
+class FooTests { }
+
+```
+
+```swift
+class FooTests: QuickSpec { }
+
+```
+
+
+
+Triggering Examples
+
+```swift
+↓class FooTests: QuickSpec { }
+↓class BarTests: QuickSpec { }
+
+```
+
+```swift
+↓class FooTests: QuickSpec { }
+↓class BarTests: QuickSpec { }
+↓class TotoTests: QuickSpec { }
+
+```
+
+
+
+
+
## Redundant Discardable Let
Identifier | Enabled by default | Supports autocorrection | Kind
diff --git a/Source/SwiftLintFramework/Models/MasterRuleList.swift b/Source/SwiftLintFramework/Models/MasterRuleList.swift
index b1ce8c90ce6..0b18c05f517 100644
--- a/Source/SwiftLintFramework/Models/MasterRuleList.swift
+++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift
@@ -75,6 +75,7 @@ public let masterRuleList = RuleList(rules: [
PrivateUnitTestRule.self,
ProhibitedSuperRule.self,
ProtocolPropertyAccessorsOrderRule.self,
+ QuickSingleSpecRule.self,
RedundantDiscardableLetRule.self,
RedundantNilCoalescingRule.self,
RedundantOptionalInitializationRule.self,
diff --git a/Source/SwiftLintFramework/Rules/QuickSingleSpecRule.swift b/Source/SwiftLintFramework/Rules/QuickSingleSpecRule.swift
new file mode 100644
index 00000000000..bbdf22bc427
--- /dev/null
+++ b/Source/SwiftLintFramework/Rules/QuickSingleSpecRule.swift
@@ -0,0 +1,57 @@
+//
+// QuickSpecLimitRule.swift
+// SwiftLint
+//
+// Created by Ornithologist Coder on 8/15/17.
+// Copyright © 2017 Realm. All rights reserved.
+//
+
+import Foundation
+import SourceKittenFramework
+
+public struct QuickSingleSpecRule: Rule, OptInRule, ConfigurationProviderRule {
+ public var configuration = SeverityConfiguration(.warning)
+
+ public init() {}
+
+ public static let description = RuleDescription(
+ identifier: "quick_single_spec",
+ name: "Quick Single Spec",
+ description: "Test files should contain a single QuickSpec class.",
+ kind: .style,
+ nonTriggeringExamples: [
+ "class FooTests { }\n",
+ "class FooTests: QuickSpec { }\n"
+ ],
+ triggeringExamples: [
+ "↓class FooTests: QuickSpec { }\n↓class BarTests: QuickSpec { }\n",
+ "↓class FooTests: QuickSpec { }\n↓class BarTests: QuickSpec { }\n↓class TotoTests: QuickSpec { }\n"
+ ]
+ )
+
+ public func validate(file: File) -> [StyleViolation] {
+ let specs = quickSpecs(in: file)
+
+ guard specs.count > 1 else { return [] }
+
+ return specs.flatMap(toViolation(in: file, configuration: configuration, numberOfSpecs: specs.count))
+ }
+
+ // MARK: - Private
+
+ private func quickSpecs(in file: File) -> [[String: SourceKitRepresentable]] {
+ return file.structure.dictionary.substructure.filter { $0.inheritedTypes.contains("QuickSpec") }
+ }
+
+ private func toViolation(in file: File,
+ configuration: SeverityConfiguration,
+ numberOfSpecs: Int) -> ([String: SourceKitRepresentable]) -> StyleViolation? {
+ return { dictionary in
+ guard let offset = dictionary.offset else { return nil }
+ return StyleViolation(ruleDescription: type(of: self).description,
+ severity: configuration.severity,
+ location: Location(file: file, byteOffset: offset),
+ reason: "\(numberOfSpecs) Quick Specs found in this file.")
+ }
+ }
+}
diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj
index b9e25b3a1e4..553104ccb81 100644
--- a/SwiftLint.xcodeproj/project.pbxproj
+++ b/SwiftLint.xcodeproj/project.pbxproj
@@ -69,6 +69,7 @@
6250D32A1ED4DFEB00735129 /* MultilineParametersRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6238AE411ED4D734006C3601 /* MultilineParametersRule.swift */; };
62622F6B1F2F2E3500D5D099 /* DiscouragedDirectInitRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62622F6A1F2F2E3500D5D099 /* DiscouragedDirectInitRule.swift */; };
626D02971F31CBCC0054788D /* XCTFailMessageRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 626D02961F31CBCC0054788D /* XCTFailMessageRule.swift */; };
+ 629C60D91F43906700B4AF92 /* QuickSingleSpecRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 629C60D81F43906700B4AF92 /* QuickSingleSpecRule.swift */; };
62A498561F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A498551F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift */; };
62A6E7931F3317E3003A0479 /* JoinedDefaultRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A6E7911F3317E3003A0479 /* JoinedDefaultRule.swift */; };
67932E2D1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67932E2C1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift */; };
@@ -377,6 +378,7 @@
6238AE411ED4D734006C3601 /* MultilineParametersRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultilineParametersRule.swift; sourceTree = ""; };
62622F6A1F2F2E3500D5D099 /* DiscouragedDirectInitRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscouragedDirectInitRule.swift; sourceTree = ""; };
626D02961F31CBCC0054788D /* XCTFailMessageRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTFailMessageRule.swift; sourceTree = ""; };
+ 629C60D81F43906700B4AF92 /* QuickSingleSpecRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickSingleSpecRule.swift; sourceTree = ""; };
62A498551F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscouragedDirectInitConfiguration.swift; sourceTree = ""; };
62A6E7911F3317E3003A0479 /* JoinedDefaultRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinedDefaultRule.swift; sourceTree = ""; };
62AF35D71F30B183009B11EE /* DiscouragedDirectInitRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscouragedDirectInitRuleTests.swift; sourceTree = ""; };
@@ -996,6 +998,7 @@
B2902A0B1D66815600BFCCF7 /* PrivateUnitTestRule.swift */,
009E09271DFEE4C200B588A7 /* ProhibitedSuperRule.swift */,
D47F31141EC918B600E3E1CA /* ProtocolPropertyAccessorsOrderRule.swift */,
+ 629C60D81F43906700B4AF92 /* QuickSingleSpecRule.swift */,
D4C889701E385B7B00BAE88D /* RedundantDiscardableLetRule.swift */,
24B4DF0B1D6DFA370097803B /* RedundantNilCoalescingRule.swift */,
D4B022951E0EF80C007E5297 /* RedundantOptionalInitializationRule.swift */,
@@ -1435,6 +1438,7 @@
D47A510E1DB29EEB00A4CC21 /* SwitchCaseOnNewlineRule.swift in Sources */,
D462021F1E15F52D0027AAD1 /* NumberSeparatorRuleExamples.swift in Sources */,
D4DA1DF41E17511D0037413D /* CompilerProtocolInitRule.swift in Sources */,
+ 629C60D91F43906700B4AF92 /* QuickSingleSpecRule.swift in Sources */,
621061BF1ED57E640082D51E /* MultilineParametersRuleExamples.swift in Sources */,
D48AE2CC1DFB58C5001C6A4A /* AttributesRulesExamples.swift in Sources */,
E88DEA6F1B09843F00A66CB0 /* Location.swift in Sources */,
diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift
index b45e64fc5da..48d803b5279 100644
--- a/Tests/LinuxMain.swift
+++ b/Tests/LinuxMain.swift
@@ -390,6 +390,7 @@ extension RulesTests {
("testPrivateUnitTest", testPrivateUnitTest),
("testProhibitedSuper", testProhibitedSuper),
("testProtocolPropertyAccessorsOrder", testProtocolPropertyAccessorsOrder),
+ ("testQuickSingleSpec", testQuickSingleSpec),
("testRedundantDiscardableLet", testRedundantDiscardableLet),
("testRedundantNilCoalescing", testRedundantNilCoalescing),
("testRedundantOptionalInitialization", testRedundantOptionalInitialization),
diff --git a/Tests/SwiftLintFrameworkTests/RulesTests.swift b/Tests/SwiftLintFrameworkTests/RulesTests.swift
index 3c0713b3d46..ca80694a780 100644
--- a/Tests/SwiftLintFrameworkTests/RulesTests.swift
+++ b/Tests/SwiftLintFrameworkTests/RulesTests.swift
@@ -237,6 +237,10 @@ class RulesTests: XCTestCase {
verifyRule(ProtocolPropertyAccessorsOrderRule.description)
}
+ func testQuickSingleSpec() {
+ verifyRule(QuickSingleSpecRule.description)
+ }
+
func testRedundantDiscardableLet() {
verifyRule(RedundantDiscardableLetRule.description)
}