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) }