diff --git a/CHANGELOG.md b/CHANGELOG.md index b22110907..74f9fba03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ ##### Enhancements +* Add `quick_discouraged_focused_test` opt-in rule which warns against + focused tests in Quick tests. + [Ornithologist Coder](https://github.com/ornithocoder) + [#1905](https://github.com/realm/SwiftLint/issues/1905) + * Add `override_in_extension` opt-in rule that warns against overriding declarations in an `extension`. [Marcelo Fabri](https://github.com/marcelofabri) diff --git a/Rules.md b/Rules.md index 5ea749554..e3a74f32c 100644 --- a/Rules.md +++ b/Rules.md @@ -79,6 +79,7 @@ * [Prohibited calls to super](#prohibited-calls-to-super) * [Protocol Property Accessors Order](#protocol-property-accessors-order) * [Quick Discouraged Call](#quick-discouraged-call) +* [Quick Discouraged Focused Test](#quick-discouraged-focused-test) * [Redundant Discardable Let](#redundant-discardable-let) * [Redundant Nil Coalescing](#redundant-nil-coalescing) * [Redundant Optional Initialization](#redundant-optional-initialization) @@ -9272,6 +9273,106 @@ class TotoTests: QuickSpec { +## Quick Discouraged Focused Test + +Identifier | Enabled by default | Supports autocorrection | Kind +--- | --- | --- | --- +`quick_discouraged_focused_test` | Disabled | No | lint + +Discouraged focused test. Other tests won't run while this one is focused. + +### Examples + +
+Non Triggering Examples + +```swift +class TotoTests: QuickSpec { + override func spec() { + describe("foo") { + describe("bar") { } + context("bar") { + it("bar") { } + } + it("bar") { } + } + } +} + +``` + +
+
+Triggering Examples + +```swift +class TotoTests: QuickSpec { + override func spec() { + ↓fdescribe("foo") { + } + } +} + +``` + +```swift +class TotoTests: QuickSpec { + override func spec() { + ↓fcontext("foo") { + } + } +} + +``` + +```swift +class TotoTests: QuickSpec { + override func spec() { + ↓fit("foo") { + } + } +} + +``` + +```swift +class TotoTests: QuickSpec { + override func spec() { + describe("foo") { + ↓fit("bar") { } + } + } +} + +``` + +```swift +class TotoTests: QuickSpec { + override func spec() { + context("foo") { + ↓fit("bar") { } + } + } +} + +``` + +```swift +class TotoTests: QuickSpec { + override func spec() { + describe("foo") { + context("bar") { + ↓fit("toto") { } + } + } +} + +``` + +
+ + + ## 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 fb01fbd85..b41bf91ce 100644 --- a/Source/SwiftLintFramework/Models/MasterRuleList.swift +++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift @@ -87,6 +87,7 @@ public let masterRuleList = RuleList(rules: [ ProhibitedSuperRule.self, ProtocolPropertyAccessorsOrderRule.self, QuickDiscouragedCallRule.self, + QuickDiscouragedFocusedTestRule.self, RedundantDiscardableLetRule.self, RedundantNilCoalescingRule.self, RedundantOptionalInitializationRule.self, diff --git a/Source/SwiftLintFramework/Rules/QuickDiscouragedFocusedTestRule.swift b/Source/SwiftLintFramework/Rules/QuickDiscouragedFocusedTestRule.swift new file mode 100644 index 000000000..7cb3e04bc --- /dev/null +++ b/Source/SwiftLintFramework/Rules/QuickDiscouragedFocusedTestRule.swift @@ -0,0 +1,130 @@ +// +// QuickDiscouragedCallRule.swift +// SwiftLint +// +// Created by Ornithologist Coder on 10/15/17. +// Copyright © 2017 Realm. All rights reserved. +// + +import Foundation +import SourceKittenFramework + +public struct QuickDiscouragedFocusedTestRule: OptInRule, ConfigurationProviderRule { + public var configuration = SeverityConfiguration(.warning) + + public init() {} + + public static let description = RuleDescription( + identifier: "quick_discouraged_focused_test", + name: "Quick Discouraged Focused Test", + description: "Discouraged focused test. Other tests won't run while this one is focused.", + kind: .lint, + nonTriggeringExamples: [ + "class TotoTests: QuickSpec {\n" + + " override func spec() {\n" + + " describe(\"foo\") {\n" + + " describe(\"bar\") { } \n" + + " context(\"bar\") {\n" + + " it(\"bar\") { }\n" + + " }\n" + + " it(\"bar\") { }\n" + + " }\n" + + " }\n" + + "}\n" + ], + triggeringExamples: [ + "class TotoTests: QuickSpec {\n" + + " override func spec() {\n" + + " ↓fdescribe(\"foo\") {\n" + + " }\n" + + " }\n" + + "}\n", + "class TotoTests: QuickSpec {\n" + + " override func spec() {\n" + + " ↓fcontext(\"foo\") {\n" + + " }\n" + + " }\n" + + "}\n", + "class TotoTests: QuickSpec {\n" + + " override func spec() {\n" + + " ↓fit(\"foo\") {\n" + + " }\n" + + " }\n" + + "}\n", + "class TotoTests: QuickSpec {\n" + + " override func spec() {\n" + + " describe(\"foo\") {\n" + + " ↓fit(\"bar\") { }\n" + + " }\n" + + " }\n" + + "}\n", + "class TotoTests: QuickSpec {\n" + + " override func spec() {\n" + + " context(\"foo\") {\n" + + " ↓fit(\"bar\") { }\n" + + " }\n" + + " }\n" + + "}\n", + "class TotoTests: QuickSpec {\n" + + " override func spec() {\n" + + " describe(\"foo\") {\n" + + " context(\"bar\") {\n" + + " ↓fit(\"toto\") { }\n" + + " }\n" + + " }\n" + + "}\n" + ] + ) + + public func validate(file: File) -> [StyleViolation] { + let testClasses = file.structure.dictionary.substructure.filter { + return $0.inheritedTypes.contains("QuickSpec") && + $0.kind.flatMap(SwiftDeclarationKind.init) == .class + } + + let specDeclarations = testClasses.flatMap { classDict in + return classDict.substructure.filter { + return $0.name == "spec()" && $0.enclosedVarParameters.isEmpty && + $0.kind.flatMap(SwiftDeclarationKind.init) == .functionMethodInstance && + $0.enclosedSwiftAttributes.contains("source.decl.attribute.override") + } + } + + return specDeclarations.flatMap { + validate(file: file, dictionary: $0) + } + } + + private func validate(file: File, dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] { + return dictionary.substructure.flatMap { subDict -> [StyleViolation] in + var violations = validate(file: file, dictionary: subDict) + + if let kindString = subDict.kind, + let kind = SwiftExpressionKind(rawValue: kindString) { + violations += validate(file: file, kind: kind, dictionary: subDict) + } + + return violations + } + } + + private func validate(file: File, + kind: SwiftExpressionKind, + dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] { + guard + kind == .call, + let name = dictionary.name, + let offset = dictionary.offset, + QuickFocusedCallKind(rawValue: name) != nil else { return [] } + + return [StyleViolation(ruleDescription: type(of: self).description, + severity: configuration.severity, + location: Location(file: file, byteOffset: offset))] + } +} + +private enum QuickFocusedCallKind: String { + case fdescribe + case fcontext + case fit +} diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index 87427d6ad..182a829c7 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -73,6 +73,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 */; }; + 627BC48D1F9405160004A6C2 /* QuickDiscouragedFocusedTestRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E54FED1F93AD57005B367B /* QuickDiscouragedFocusedTestRule.swift */; }; 629C60D91F43906700B4AF92 /* SingleTestClassRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 629C60D81F43906700B4AF92 /* SingleTestClassRule.swift */; }; 62A498561F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A498551F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift */; }; 62A6E7931F3317E3003A0479 /* JoinedDefaultRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A6E7911F3317E3003A0479 /* JoinedDefaultRule.swift */; }; @@ -410,6 +411,7 @@ 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 = ""; }; + 62E54FED1F93AD57005B367B /* QuickDiscouragedFocusedTestRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickDiscouragedFocusedTestRule.swift; sourceTree = ""; }; 65454F451B14D73800319A6C /* ControlStatementRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControlStatementRule.swift; sourceTree = ""; }; 67932E2C1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CyclomaticComplexityConfigurationTests.swift; sourceTree = ""; }; 67EB4DF81E4CC101004E9ACD /* CyclomaticComplexityConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CyclomaticComplexityConfiguration.swift; sourceTree = ""; }; @@ -1020,7 +1022,6 @@ D4C0E46E1E3D973600C560F2 /* ForWhereRule.swift */, E88DEA8F1B099A3100A66CB0 /* FunctionBodyLengthRule.swift */, 2E5761A91C573B83003271AF /* FunctionParameterCountRule.swift */, - 827169B41F48D712003FB9AF /* NoGroupingExtensionRule.swift */, D4B022A31E105636007E5297 /* GenericTypeNameRule.swift */, E88DEA931B099C0900A66CB0 /* IdentifierNameRule.swift */, D4130D961E16183F00242361 /* IdentifierNameRuleExamples.swift */, @@ -1049,6 +1050,7 @@ E88DEA951B099CF200A66CB0 /* NestingRule.swift */, D4DAE8BB1DE14E8F00B0AE7A /* NimbleOperatorRule.swift */, 1E18574A1EADBA51004F89F7 /* NoExtensionAccessModifierRule.swift */, + 827169B41F48D712003FB9AF /* NoGroupingExtensionRule.swift */, D4DABFD61E2C23B1009617B6 /* NotificationCenterDetachmentRule.swift */, D4DABFD81E2C59BC009617B6 /* NotificationCenterDetachmentRuleExamples.swift */, D46252531DF63FB200BE2CA1 /* NumberSeparatorRule.swift */, @@ -1057,8 +1059,8 @@ 692B1EB11BD7E00F00EAABFF /* OpeningBraceRule.swift */, E5A167C81B25A0B000CF2D03 /* OperatorFunctionWhitespaceRule.swift */, D4FBADCF1E00DA0400669C73 /* OperatorUsageWhitespaceRule.swift */, - D40FE89C1F867BFF006433E2 /* OverrideInExtensionRule.swift */, 78F032441D7C877800BE709A /* OverriddenSuperCallRule.swift */, + D40FE89C1F867BFF006433E2 /* OverrideInExtensionRule.swift */, D403A4A21F4DB5510020CA02 /* PatternMatchingKeywordsRule.swift */, 094385021D5D4F78009168CF /* PrivateOutletRule.swift */, 1E3C2D701EE36C6F00C8386D /* PrivateOverFilePrivateRule.swift */, @@ -1067,12 +1069,14 @@ D47F31141EC918B600E3E1CA /* ProtocolPropertyAccessorsOrderRule.swift */, 623E36EF1F3DB1B1002E5B71 /* QuickDiscouragedCallRule.swift */, 623E36F11F3DB988002E5B71 /* QuickDiscouragedCallRuleExamples.swift */, + 62E54FED1F93AD57005B367B /* QuickDiscouragedFocusedTestRule.swift */, D4C889701E385B7B00BAE88D /* RedundantDiscardableLetRule.swift */, 24B4DF0B1D6DFA370097803B /* RedundantNilCoalescingRule.swift */, D4B022951E0EF80C007E5297 /* RedundantOptionalInitializationRule.swift */, D41E7E0A1DF9DABB0065259A /* RedundantStringEnumValueRule.swift */, D4B022B11E10B613007E5297 /* RedundantVoidReturnRule.swift */, E57B23C01B1D8BF000DEA512 /* ReturnArrowWhitespaceRule.swift */, + 3BCC04CE1C4F56D3006073C3 /* RuleConfigurations */, D4D5A5FE1E1F3A1C00D15E0C /* ShorthandOperatorRule.swift */, 629C60D81F43906700B4AF92 /* SingleTestClassRule.swift */, D286EC001E02DA190003CF72 /* SortedImportsRule.swift */, @@ -1103,7 +1107,6 @@ D47079AE1DFE520000027086 /* VoidReturnRule.swift */, 094384FF1D5D2382009168CF /* WeakDelegateRule.swift */, 626D02961F31CBCC0054788D /* XCTFailMessageRule.swift */, - 3BCC04CE1C4F56D3006073C3 /* RuleConfigurations */, ); path = Rules; sourceTree = ""; @@ -1550,6 +1553,7 @@ E88198421BEA929F00333A11 /* NestingRule.swift in Sources */, D46A317F1F1CEDCD00AF914A /* UnneededParenthesesInClosureArgumentRule.swift in Sources */, D4470D591EB6B4D1008A1B2E /* EmptyEnumArgumentsRule.swift in Sources */, + 627BC48D1F9405160004A6C2 /* QuickDiscouragedFocusedTestRule.swift in Sources */, 3BB47D851C51D80000AE6A10 /* NSRegularExpression+SwiftLint.swift in Sources */, E881985B1BEA974E00333A11 /* StatementPositionRule.swift in Sources */, B58AEED61C492C7B00E901FD /* ForceUnwrappingRule.swift in Sources */, diff --git a/Tests/SwiftLintFrameworkTests/RulesTests.swift b/Tests/SwiftLintFrameworkTests/RulesTests.swift index 6b5831106..c5372668b 100644 --- a/Tests/SwiftLintFrameworkTests/RulesTests.swift +++ b/Tests/SwiftLintFrameworkTests/RulesTests.swift @@ -282,6 +282,10 @@ class RulesTests: XCTestCase { verifyRule(QuickDiscouragedCallRule.description) } + func testQuickDiscouragedFocusedTest() { + verifyRule(QuickDiscouragedFocusedTestRule.description) + } + func testRedundantDiscardableLet() { verifyRule(RedundantDiscardableLetRule.description) }