Files
SwiftLint/Source/SwiftLintFramework/Rules/Lint/PrivateUnitTestRule.swift
T

181 lines
6.5 KiB
Swift

import Foundation
import SourceKittenFramework
private extension SourceKittenDictionary {
var superclass: String? {
guard declarationKind == .class,
let className = inheritedTypes.first else { return nil }
return className
}
var parameters: [SourceKittenDictionary] {
return substructure.filter { dict in
guard let kind = dict.declarationKind else {
return false
}
return kind == .varParameter
}
}
}
public struct PrivateUnitTestRule: ASTRule, ConfigurationProviderRule, CacheDescriptionProvider, AutomaticTestableRule {
public var configuration: PrivateUnitTestConfiguration = {
var configuration = PrivateUnitTestConfiguration(identifier: "private_unit_test")
configuration.message = "Unit test marked `private` will not be run by XCTest."
configuration.regex = regex("XCTestCase")
return configuration
}()
internal var cacheDescription: String {
return configuration.cacheDescription
}
public init() {}
public static let description = RuleDescription(
identifier: "private_unit_test",
name: "Private Unit Test",
description: "Unit tests marked private are silently skipped.",
kind: .lint,
nonTriggeringExamples: [
Example("""
"class FooTest: XCTestCase {
func test1() {}
internal func test2() {}
public func test3() {}
}
"""),
Example("""
internal class FooTest: XCTestCase {
func test1() {}
internal func test2() {}
public func test3() {}
}
"""),
Example("""
public class FooTest: XCTestCase {
func test1() {}
internal func test2() {}
public func test3() {}
}
"""),
Example("""
@objc private class FooTest: XCTestCase {
@objc private func test1() {}
internal func test2() {}
public func test3() {}
}
"""),
// Non-test classes
Example("""
private class Foo: NSObject {
func test1() {}
internal func test2() {}
public func test3() {}
}
"""),
Example("""
private class Foo {
func test1() {}
internal func test2() {}
public func test3() {}
}
"""),
// Methods with params
Example("""
public class FooTest: XCTestCase {
func test1(param: Int) {}
}
""")
],
triggeringExamples: [
Example("""
private class FooTest: XCTestCase {
func test1() {}
internal func test2() {}
public func test3() {}
private func test4() {}
}
"""),
Example("""
class FooTest: XCTestCase {
func test1() {}
internal func test2() {}
public func test3() {}
private func test4() {}
}
"""),
Example("""
internal class FooTest: XCTestCase {
func test1() {}
internal func test2() {}
public func test3() {}
private func test4() {}
}
"""),
Example("""
public class FooTest: XCTestCase {
func test1() {}
internal func test2() {}
public func test3() {}
private func test4() {}
}
""")
]
)
public func validate(file: SwiftLintFile, kind: SwiftDeclarationKind,
dictionary: SourceKittenDictionary) -> [StyleViolation] {
guard kind == .class && isTestClass(dictionary) else { return [] }
/* It's not strictly necessary to check for `private` on classes because a
private class will result in `private` on all its members in the AST.
However, it's still useful to check the class explicitly because this
gives us a more clear error message. If we check only methods, the line
number of the error will be that of the function, which may not
necessarily be marked `private` but inherited it from the class access
modifier. By checking the class we ensure the line nuber we report for
the violation will match the line that must be edited.
*/
let classViolations = validateAccessControlLevel(file: file, dictionary: dictionary)
guard classViolations.isEmpty else { return classViolations }
return dictionary.substructure.flatMap { subDict -> [StyleViolation] in
return validateFunction(file: file, dictionary: subDict)
}
}
private func isTestClass(_ dictionary: SourceKittenDictionary) -> Bool {
guard let regex = configuration.regex, let superclass = dictionary.superclass else {
return false
}
let range = superclass.fullNSRange
return regex.matches(in: superclass, options: [], range: range).isNotEmpty
}
private func validateFunction(file: SwiftLintFile,
dictionary: SourceKittenDictionary) -> [StyleViolation] {
guard let kind = dictionary.declarationKind,
kind == .functionMethodInstance,
let name = dictionary.name, name.hasPrefix("test"),
dictionary.parameters.isEmpty else {
return []
}
return validateAccessControlLevel(file: file, dictionary: dictionary)
}
private func validateAccessControlLevel(file: SwiftLintFile,
dictionary: SourceKittenDictionary) -> [StyleViolation] {
guard let acl = dictionary.accessibility, acl.isPrivate,
!dictionary.enclosedSwiftAttributes.contains(.objc)
else { return [] }
let offset = dictionary.offset ?? 0
return [StyleViolation(ruleDescription: Self.description,
severity: configuration.severityConfiguration.severity,
location: Location(file: file, byteOffset: offset),
reason: configuration.message)]
}
}