import SwiftSyntax @SwiftSyntaxRule(optIn: true) struct BalancedXCTestLifecycleRule: Rule { var configuration = BalancedXCTestLifecycleConfiguration() static let description = RuleDescription( identifier: "balanced_xctest_lifecycle", name: "Balanced XCTest Life Cycle", description: "Test classes must implement balanced setUp and tearDown methods", rationale: """ The `setUp` method of `XCTestCase` can be used to set up variables and resources before \ each test is run (or with the `class` variant, before all tests are run). This rule verifies that every class with an implementation of a `setUp` method also has \ a `tearDown` method (and vice versa). The `tearDown` method should be used to cleanup or reset any resources that could \ otherwise have any effects on subsequent tests, and to free up any instance variables. """, 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: - Private private extension BalancedXCTestLifecycleRule { final class Visitor: ViolationsSyntaxVisitor { override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { .all } override func visitPost(_ node: ClassDeclSyntax) { guard node.isXCTestCase(configuration.testParentClasses) else { return } let methods = SetupTearDownVisitor(configuration: configuration, file: file) .walk(tree: node.memberBlock, handler: \.methods) guard methods.contains(.setUp) != methods.contains(.tearDown) else { return } violations.append(node.name.positionAfterSkippingLeadingTrivia) } } } private final class SetupTearDownVisitor: ViolationsSyntaxVisitor { override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { .all } private(set) var methods: Set = [] override func visitPost(_ node: FunctionDeclSyntax) { if let method = XCTMethod(node.name.description), node.signature.parameterClause.parameters.isEmpty { methods.insert(method) } } } 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 } } }