Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add test_parent_classes option to single_test_class, balanced_xctest_lifecycle, and empty_xctest rules #4233

5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@
[Martin Redington](https://github.com/mildm8nnered)
[#4200](https://github.com/realm/SwiftLint/issues/4200)

* Add `test_parent_classes` option to `balanced_xctest_lifecycle`, `single_test_class`
and `empty_xctest_method` rules.
[Martin Redington](https://github.com/mildm8nnered)
[#4200](https://github.com/realm/SwiftLint/issues/4200)

#### Bug Fixes

* Respect `validates_start_with_lowercase` option when linting function names.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import SourceKittenFramework
public struct BalancedXCTestLifecycleRule: Rule, OptInRule, ConfigurationProviderRule {
// MARK: - Properties

public var configuration = SeverityConfiguration(.warning)
public var configuration = BalancedXCTestLifecycleConfiguration()

public static let description = RuleDescription(
identifier: "balanced_xctest_lifecycle",
Expand Down Expand Up @@ -120,8 +120,8 @@ public struct BalancedXCTestLifecycleRule: Rule, OptInRule, ConfigurationProvide

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")
dictionary.declarationKind == .class &&
configuration.testParentClasses.intersection(dictionary.inheritedTypes).isNotEmpty
}
}

Expand Down
31 changes: 23 additions & 8 deletions Source/SwiftLintFramework/Rules/Lint/EmptyXCTestMethodRule.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import SwiftSyntax

public struct EmptyXCTestMethodRule: OptInRule, ConfigurationProviderRule, SwiftSyntaxRule {
public var configuration = SeverityConfiguration(.warning)
public var configuration = EmptyXCTestMethodConfiguration()

public init() {}

Expand All @@ -15,15 +15,28 @@ public struct EmptyXCTestMethodRule: OptInRule, ConfigurationProviderRule, Swift
)

public func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor? {
EmptyXCTestMethodRuleVisitor()
EmptyXCTestMethodRuleVisitor(testParentClasses: configuration.testParentClasses)
}

public func makeViolation(file: SwiftLintFile, position: AbsolutePosition) -> StyleViolation {
StyleViolation(
ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file, position: position)
)
}
}

private final class EmptyXCTestMethodRuleVisitor: SyntaxVisitor, ViolationsSyntaxVisitor {
private(set) var violationPositions: [AbsolutePosition] = []
private let testParentClasses: Set<String>

init(testParentClasses: Set<String>) {
self.testParentClasses = testParentClasses
}

override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
node.isXCTestCase ? .visitChildren : .skipChildren
isNodeATestCase(node) ? .visitChildren : .skipChildren
}

override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
Expand All @@ -43,16 +56,18 @@ private final class EmptyXCTestMethodRuleVisitor: SyntaxVisitor, ViolationsSynta
violationPositions.append(node.funcKeyword.positionAfterSkippingLeadingTrivia)
}
}

private func isNodeATestCase(_ node: ClassDeclSyntax) -> Bool {
testParentClasses.intersection(node.inheritedTypes).isNotEmpty
}
}

private extension ClassDeclSyntax {
var isXCTestCase: Bool {
var inheritedTypes: [String] {
guard let inheritanceList = inheritanceClause?.inheritedTypeCollection else {
return false
}
return inheritanceList.contains { type in
type.typeName.as(SimpleTypeIdentifierSyntax.self)?.name.text == "XCTestCase"
return []
}
return inheritanceList.compactMap { $0.typeName.as(SimpleTypeIdentifierSyntax.self)?.name.text }
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
public struct BalancedXCTestLifecycleConfiguration: RuleConfiguration, Equatable {
public private(set) var severityConfiguration = SeverityConfiguration(.warning)
public private(set) var testParentClasses: Set<String> = ["XCTestCase"]

public var consoleDescription: String {
return severityConfiguration.consoleDescription +
", test_parent_classes: [\(testParentClasses)]"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The brackets are superfluous. Sorting the set makes the output deterministic:

Suggested change
", test_parent_classes: [\(testParentClasses)]"
", test_parent_classes: \(testParentClasses.sorted())"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I added QuickSpec to all the defaults, and rolled the new ones up, with typealiases for the names you'd expect.

But I bodged rebasing, so I just moved to a new branch and PR - #4262

}

public mutating func apply(configuration: Any) throws {
guard let configuration = configuration as? [String: Any] else {
throw ConfigurationError.unknownConfiguration
}

if let severityString = configuration["severity"] as? String {
try severityConfiguration.apply(configuration: severityString)
}

if let extraTestParentClasses = configuration["test_parent_classes"] as? [String] {
self.testParentClasses.formUnion(extraTestParentClasses)
}
}

public var severity: ViolationSeverity {
return severityConfiguration.severity
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
public struct EmptyXCTestMethodConfiguration: RuleConfiguration, Equatable {
public private(set) var severityConfiguration = SeverityConfiguration(.warning)
public private(set) var testParentClasses: Set<String> = ["XCTestCase"]

public var consoleDescription: String {
return severityConfiguration.consoleDescription +
", test_parent_classes: [\(testParentClasses)]"
}

public mutating func apply(configuration: Any) throws {
guard let configuration = configuration as? [String: Any] else {
throw ConfigurationError.unknownConfiguration
}

if let severityString = configuration["severity"] as? String {
try severityConfiguration.apply(configuration: severityString)
}

if let extraTestParentClasses = configuration["test_parent_classes"] as? [String] {
self.testParentClasses.formUnion(extraTestParentClasses)
}
}

public var severity: ViolationSeverity {
return severityConfiguration.severity
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
public struct SingleTestClassConfiguration: RuleConfiguration, Equatable {
public private(set) var severityConfiguration = SeverityConfiguration(.warning)
public private(set) var testParentClasses: Set<String> = ["QuickSpec", "XCTestCase"]

public var consoleDescription: String {
return severityConfiguration.consoleDescription +
", test_parent_classes: [\(testParentClasses)]"
}

public mutating func apply(configuration: Any) throws {
guard let configuration = configuration as? [String: Any] else {
throw ConfigurationError.unknownConfiguration
}

if let severityString = configuration["severity"] as? String {
try severityConfiguration.apply(configuration: severityString)
}

if let extraTestParentClasses = configuration["test_parent_classes"] as? [String] {
self.testParentClasses.formUnion(extraTestParentClasses)
}
}

public var severity: ViolationSeverity {
return severityConfiguration.severity
}
}
11 changes: 4 additions & 7 deletions Source/SwiftLintFramework/Rules/Style/SingleTestClassRule.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import SourceKittenFramework

public struct SingleTestClassRule: Rule, OptInRule, ConfigurationProviderRule {
public var configuration = SeverityConfiguration(.warning)
public var configuration = SingleTestClassConfiguration()

public static let description = RuleDescription(
identifier: "single_test_class",
Expand Down Expand Up @@ -44,8 +44,6 @@ public struct SingleTestClassRule: Rule, OptInRule, ConfigurationProviderRule {
]
)

private let testClasses: Set = ["QuickSpec", "XCTestCase"]

public init() {}

public func validate(file: SwiftLintFile) -> [StyleViolation] {
Expand All @@ -64,10 +62,9 @@ public struct SingleTestClassRule: Rule, OptInRule, ConfigurationProviderRule {
}

private func testClasses(in file: SwiftLintFile) -> [SourceKittenDictionary] {
let dict = file.structureDictionary
return dict.substructure.filter { dictionary in
guard dictionary.declarationKind == .class else { return false }
return !testClasses.isDisjoint(with: dictionary.inheritedTypes)
return file.structureDictionary.substructure.filter { dictionary in
dictionary.declarationKind == .class &&
configuration.testParentClasses.intersection(dictionary.inheritedTypes).isNotEmpty
}
}
}