diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ed23fac40..91a66fbc77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,9 +48,10 @@ `assert(false)`. [Otavio Cordeiro](https://github.com/otaviocc) -* Add opt-in rule `private_subject` rule which warns against - public Combine subjects. +* Adds `balanced_xctest_lifecycle` opt-in rule to enforce balanced `setUp` + and `tearDown` methods in a test class. [Otavio Cordeiro](https://github.com/otaviocc) + [#3452](https://github.com/realm/SwiftLint/issues/3452) * Tweak the auto-correction result console output for clarity. [mokagio](https://github.com/mokagio) @@ -91,6 +92,10 @@ variable when running analyze. [jpsim](https://github.com/jpsim) +* Add opt-in rule `private_subject` rule which warns against + public Combine subjects. + [Otavio Cordeiro](https://github.com/otaviocc) + #### Bug Fixes * Fix `custom_rules` merging when the parent configuration is based on diff --git a/Source/SwiftLintFramework/Models/PrimaryRuleList.swift b/Source/SwiftLintFramework/Models/PrimaryRuleList.swift index 0864a76da9..f7075361b1 100644 --- a/Source/SwiftLintFramework/Models/PrimaryRuleList.swift +++ b/Source/SwiftLintFramework/Models/PrimaryRuleList.swift @@ -155,6 +155,7 @@ public let primaryRuleList = RuleList(rules: [ RedundantVoidReturnRule.self, RequiredDeinitRule.self, RequiredEnumCaseRule.self, + BalancedXCTestLifecycleRule.self, ReturnArrowWhitespaceRule.self, ShorthandOperatorRule.self, SingleTestClassRule.self, diff --git a/Source/SwiftLintFramework/Rules/Lint/BalancedXCTestLifecycleRule.swift b/Source/SwiftLintFramework/Rules/Lint/BalancedXCTestLifecycleRule.swift new file mode 100644 index 0000000000..31cf580847 --- /dev/null +++ b/Source/SwiftLintFramework/Rules/Lint/BalancedXCTestLifecycleRule.swift @@ -0,0 +1,159 @@ +import SourceKittenFramework + +public struct BalancedXCTestLifecycleRule: Rule, OptInRule, ConfigurationProviderRule, AutomaticTestableRule { + // MARK: - Properties + + public var configuration = SeverityConfiguration(.warning) + + public static let description = RuleDescription( + identifier: "balanced_xctest_lifecycle", + name: "Balanced XCTest life-cycle", + description: "Test classes must implement balanced setUp and tearDown methods.", + kind: .lint, + nonTriggeringExamples: [ + Example(#""" + final class FooTests: XCTestCase { + override func setUp() {} + override func tearDown() {} + } + """#), + Example(#""" + final class FooTests: XCTestCase { + override func setUpWithError() throws {} + override func tearDown() {} + } + """#), + Example(#""" + final class FooTests: XCTestCase { + override func setUp() {} + override func tearDownWithError() throws {} + } + """#), + Example(#""" + final class FooTests: XCTestCase { + override func setUpWithError() throws {} + override func tearDownWithError() throws {} + } + final class BarTests: XCTestCase { + override func setUpWithError() throws {} + override func tearDownWithError() throws {} + } + """#), + Example(#""" + struct FooTests { + override func setUp() {} + } + class BarTests { + override func setUpWithError() throws {} + } + """#), + Example(#""" + final class FooTests: XCTestCase { + override func setUpAlLExamples() {} + } + """#), + Example(#""" + final class FooTests: XCTestCase { + class func setUp() {} + class func tearDown() {} + } + """#) + ], + triggeringExamples: [ + Example(#""" + final class ↓FooTests: XCTestCase { + override func setUp() {} + } + """#), + Example(#""" + final class ↓FooTests: XCTestCase { + override func setUpWithError() throws {} + } + """#), + Example(#""" + final class FooTests: XCTestCase { + override func setUp() {} + override func tearDownWithError() throws {} + } + final class ↓BarTests: XCTestCase { + override func setUpWithError() throws {} + } + """#), + Example(#""" + final class ↓FooTests: XCTestCase { + class func tearDown() {} + } + """#), + Example(#""" + final class ↓FooTests: XCTestCase { + override func tearDown() {} + } + """#), + Example(#""" + final class ↓FooTests: XCTestCase { + override func tearDownWithError() throws {} + } + """#), + Example(#""" + final class FooTests: XCTestCase { + override func setUp() {} + override func tearDownWithError() throws {} + } + final class ↓BarTests: XCTestCase { + override func tearDownWithError() throws {} + } + """#) + ] + ) + + // MARK: - Life cycle + + public init() {} + + // MARK: - Public + + public func validate(file: SwiftLintFile) -> [StyleViolation] { + testClasses(in: file).compactMap { violations(in: file, for: $0) } + } + + // MARK: - Private + + private func testClasses(in file: SwiftLintFile) -> [SourceKittenDictionary] { + file.structureDictionary.substructure.filter { dictionary in + guard dictionary.declarationKind == .class else { return false } + return dictionary.inheritedTypes.contains("XCTestCase") + } + } + + private func violations(in file: SwiftLintFile, + for dictionary: SourceKittenDictionary) -> StyleViolation? { + let methods = dictionary.substructure + .compactMap { XCTMethod($0.name) } + + guard + methods.contains(.setUp) != methods.contains(.tearDown), + let offset = dictionary.nameOffset + else { + return nil + } + + return StyleViolation(ruleDescription: Self.description, + severity: configuration.severity, + location: Location(file: file, byteOffset: offset)) + } +} + +// MARK: - Private + +private enum XCTMethod { + case setUp + case tearDown + + init?(_ name: String?) { + switch name { + case "setUp()", "setUpWithError()": self = .setUp + case "tearDown()", "tearDownWithError()": self = .tearDown + default: return nil + } + } +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index c4febefede..54a234dbc2 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,6 +1,5 @@ -// Generated using Sourcery 1.0.2 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT - @testable import SwiftLintFrameworkTests import XCTest @@ -34,6 +33,12 @@ extension AttributesRuleTests { ] } +extension BalancedXCTestLifecycleRuleTests { + static var allTests: [(String, (BalancedXCTestLifecycleRuleTests) -> () throws -> Void)] = [ + ("testWithDefaultConfiguration", testWithDefaultConfiguration) + ] +} + extension BlockBasedKVORuleTests { static var allTests: [(String, (BlockBasedKVORuleTests) -> () throws -> Void)] = [ ("testWithDefaultConfiguration", testWithDefaultConfiguration) @@ -185,12 +190,15 @@ extension ConfigurationTests { ("testInitWithRelativePathAndRootPath", testInitWithRelativePathAndRootPath), ("testEnableAllRulesConfiguration", testEnableAllRulesConfiguration), ("testOnlyRules", testOnlyRules), + ("testOnlyRulesWithCustomRules", testOnlyRulesWithCustomRules), ("testWarningThreshold_value", testWarningThreshold_value), ("testWarningThreshold_nil", testWarningThreshold_nil), ("testOtherRuleConfigurationsAlongsideOnlyRules", testOtherRuleConfigurationsAlongsideOnlyRules), ("testDisabledRules", testDisabledRules), ("testDisabledRulesWithUnknownRule", testDisabledRulesWithUnknownRule), ("testDuplicatedRules", testDuplicatedRules), + ("testIncludedExcludedRelativeLocationLevel1", testIncludedExcludedRelativeLocationLevel1), + ("testIncludedExcludedRelativeLocationLevel0", testIncludedExcludedRelativeLocationLevel0), ("testExcludedPaths", testExcludedPaths), ("testForceExcludesFile", testForceExcludesFile), ("testForceExcludesFileNotPresentInExcluded", testForceExcludesFileNotPresentInExcluded), @@ -220,6 +228,9 @@ extension ConfigurationTests { ("testOnlyRulesMerging", testOnlyRulesMerging), ("testCustomRulesMerging", testCustomRulesMerging), ("testMergingAllowsDisablingParentsCustomRules", testMergingAllowsDisablingParentsCustomRules), + ("testCustomRulesMergingWithOnlyRulesCase1", testCustomRulesMergingWithOnlyRulesCase1), + ("testCustomRulesMergingWithOnlyRulesCase2", testCustomRulesMergingWithOnlyRulesCase2), + ("testCustomRulesReconfiguration", testCustomRulesReconfiguration), ("testLevel0", testLevel0), ("testLevel1", testLevel1), ("testLevel2", testLevel2), @@ -1785,6 +1796,7 @@ XCTMain([ testCase(AnyObjectProtocolRuleTests.allTests), testCase(ArrayInitRuleTests.allTests), testCase(AttributesRuleTests.allTests), + testCase(BalancedXCTestLifecycleRuleTests.allTests), testCase(BlockBasedKVORuleTests.allTests), testCase(ClassDelegateProtocolRuleTests.allTests), testCase(ClosingBraceRuleTests.allTests), diff --git a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift index 3f44064e21..591ccbbc1d 100644 --- a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift +++ b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift @@ -1,6 +1,5 @@ -// Generated using Sourcery 1.0.2 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 1.2.0 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT - import SwiftLintFramework import XCTest @@ -18,6 +17,12 @@ class ArrayInitRuleTests: XCTestCase { } } +class BalancedXCTestLifecycleRuleTests: XCTestCase { + func testWithDefaultConfiguration() { + verifyRule(BalancedXCTestLifecycleRule.description) + } +} + class BlockBasedKVORuleTests: XCTestCase { func testWithDefaultConfiguration() { verifyRule(BlockBasedKVORule.description)