Skip to content

Commit

Permalink
Add new final_test_case rule (#5396)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimplyDanny authored Dec 20, 2023
1 parent 9ed8020 commit ddaf3d2
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 1 deletion.
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ disabled_rules:
- explicit_top_level_acl
- explicit_type_interface
- file_types_order
- final_test_case
- force_unwrapping
- function_default_parameter_at_end
- implicit_return
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
* Print invalid keys when configuration parsing fails.
[SimplyDanny](https://github.com/SimplyDanny)
[#5347](https://github.com/realm/SwiftLint/pull/5347)

* Add new `final_test_case` rule that triggers on non-final test classes.
[SimplyDanny](https://github.com/SimplyDanny)

* Allow to configure more operators in `identifier_name` rule. The new option
is named `additional_operators`. Use it to add more operators to the list
Expand Down
1 change: 1 addition & 0 deletions Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public let builtInRules: [any Rule.Type] = [
FileNameNoSpaceRule.self,
FileNameRule.self,
FileTypesOrderRule.self,
FinalTestCaseRule.self,
FirstWhereRule.self,
FlatMapOverMapReduceRule.self,
ForWhereRule.self,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import SwiftLintCore
import SwiftSyntax

@SwiftSyntaxRule
struct FinalTestCaseRule: SwiftSyntaxCorrectableRule, OptInRule {
var configuration = FinalTestCaseConfiguration()

static var description = RuleDescription(
identifier: "final_test_case",
name: "Final Test Case",
description: "Test cases should be final",
kind: .performance,
nonTriggeringExamples: [
Example("final class Test: XCTestCase {}"),
Example("open class Test: XCTestCase {}"),
Example("public final class Test: QuickSpec {}"),
Example("class Test: MyTestCase {}"),
Example("struct Test: MyTestCase {}", configuration: ["test_parent_classes": "MyTestCase"])
],
triggeringExamples: [
Example("class ↓Test: XCTestCase {}"),
Example("public class ↓Test: QuickSpec {}"),
Example("class ↓Test: MyTestCase {}", configuration: ["test_parent_classes": "MyTestCase"])
],
corrections: [
Example("class ↓Test: XCTestCase {}"):
Example("final class Test: XCTestCase {}"),
Example("internal class ↓Test: XCTestCase {}"):
Example("internal final class Test: XCTestCase {}")
]
)

func makeRewriter(file: SwiftLintFile) -> (some ViolationsSyntaxRewriter)? {
Rewriter(
configuration: configuration,
locationConverter: file.locationConverter,
disabledRegions: disabledRegions(file: file)
)
}
}

private extension FinalTestCaseRule {
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
override func visitPost(_ node: ClassDeclSyntax) {
if node.isNonFinalTestClass(parentClasses: configuration.testParentClasses) {
violations.append(node.name.positionAfterSkippingLeadingTrivia)
}
}
}

final class Rewriter: ViolationsSyntaxRewriter {
private let configuration: FinalTestCaseConfiguration

init(configuration: FinalTestCaseConfiguration,
locationConverter: SourceLocationConverter,
disabledRegions: [SourceRange]) {
self.configuration = configuration
super.init(locationConverter: locationConverter, disabledRegions: disabledRegions)
}

override func visit(_ node: ClassDeclSyntax) -> DeclSyntax {
var newNode = node
if node.isNonFinalTestClass(parentClasses: configuration.testParentClasses) {
correctionPositions.append(node.name.positionAfterSkippingLeadingTrivia)
let finalModifier = DeclModifierSyntax(name: .keyword(.final))
newNode =
if node.modifiers.isEmpty {
node
.with(\.modifiers, [finalModifier.with(\.leadingTrivia, node.classKeyword.leadingTrivia)])
.with(\.classKeyword.leadingTrivia, .space)
} else {
node
.with(\.modifiers, node.modifiers + [finalModifier.with(\.trailingTrivia, .space)])
}
}
return super.visit(newNode)
}
}
}

private extension ClassDeclSyntax {
func isNonFinalTestClass(parentClasses: Set<String>) -> Bool {
inheritanceClause.containsInheritedType(inheritedTypes: parentClasses)
&& !modifiers.contains(keyword: .open)
&& !modifiers.contains(keyword: .final)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import SwiftLintCore

typealias BalancedXCTestLifecycleConfiguration = UnitTestConfiguration<BalancedXCTestLifecycleRule>
typealias EmptyXCTestMethodConfiguration = UnitTestConfiguration<EmptyXCTestMethodRule>
typealias SingleTestClassConfiguration = UnitTestConfiguration<SingleTestClassRule>
typealias FinalTestCaseConfiguration = UnitTestConfiguration<FinalTestCaseRule>
typealias NoMagicNumbersConfiguration = UnitTestConfiguration<NoMagicNumbersRule>
typealias SingleTestClassConfiguration = UnitTestConfiguration<SingleTestClassRule>

@AutoApply
struct UnitTestConfiguration<Parent: Rule>: SeverityBasedRuleConfiguration {
Expand Down
6 changes: 6 additions & 0 deletions Tests/GeneratedTests/GeneratedTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,12 @@ class FileTypesOrderRuleGeneratedTests: SwiftLintTestCase {
}
}

class FinalTestCaseRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(FinalTestCaseRule.description)
}
}

class FirstWhereRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(FirstWhereRule.description)
Expand Down

0 comments on commit ddaf3d2

Please sign in to comment.