Files
SwiftLint/Source/SwiftLintFramework/Rules/PrivateUnitTestRule.swift
T
Norio Nomura 48b25b6aab Merge commit '83830861d6c6b7fee3218461f699c7d3b457bf2a' into swift3.0
* commit '83830861d6c6b7fee3218461f699c7d3b457bf2a':
  refactor isPrivateLevel(identifier:)
  refactor nameStrippingLeadingUnderscoreIfPrivate
  combine changelog entries for #781 and #831
  Add support for filePrivate and open in rules

# Conflicts:
#	Source/SwiftLintFramework/Extensions/String+SwiftLint.swift
#	Source/SwiftLintFramework/Rules/MissingDocsRule.swift
2016-11-30 20:16:47 +09:00

183 lines
6.9 KiB
Swift

//
// ClassVisibilityRule.swift
// SwiftLint
//
// Created by Cristian Filipov on 8/3/16.
// Copyright © 2016 Realm. All rights reserved.
//
import Foundation
import SourceKittenFramework
private extension AccessControlLevel {
init?(_ dictionary: [String: SourceKitRepresentable]) {
guard let
accessibility = dictionary["key.accessibility"] as? String,
let acl = AccessControlLevel(rawValue: accessibility)
else { return nil }
self = acl
}
}
func superclass(_ dictionary: [String: SourceKitRepresentable]) -> String? {
typealias SKArray = [SourceKitRepresentable]
typealias SKDict = [String: SourceKitRepresentable]
guard let
kindString = dictionary["key.kind"] as? String,
let kind = SwiftDeclarationKind(rawValue: kindString), kind == .class
else { return nil }
guard let
inheritedTypes = dictionary["key.inheritedtypes"] as? SKArray,
let className = (inheritedTypes[0] as? SKDict)?["key.name"] as? String
else { return nil }
return className
}
open class FooTest: NSObject { }
public struct PrivateUnitTestRule: ASTRule, ConfigurationProviderRule {
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
}()
public init() {}
public static let description = RuleDescription(
identifier: "private_unit_test",
name: "Private Unit Test",
description: "Unit tests marked private are silently skipped.",
nonTriggeringExamples: [
"class FooTest: XCTestCase { " +
"func test1() {}\n " +
"internal func test2() {}\n " +
"public func test3() {}\n " +
"}",
"internal class FooTest: XCTestCase { " +
"func test1() {}\n " +
"internal func test2() {}\n " +
"public func test3() {}\n " +
"}",
"public class FooTest: XCTestCase { " +
"func test1() {}\n " +
"internal func test2() {}\n " +
"public func test3() {}\n " +
"}",
// Non-test classes
"private class Foo: NSObject { " +
"func test1() {}\n " +
"internal func test2() {}\n " +
"public func test3() {}\n " +
"}",
"private class Foo { " +
"func test1() {}\n " +
"internal func test2() {}\n " +
"public func test3() {}\n " +
"}"
],
triggeringExamples: [
"private ↓class FooTest: XCTestCase { " +
"func test1() {}\n " +
"internal func test2() {}\n " +
"public func test3() {}\n " +
"private func test4() {}\n " +
"}",
"class FooTest: XCTestCase { " +
"func test1() {}\n " +
"internal func test2() {}\n " +
"public func test3() {}\n " +
"private ↓func test4() {}\n " +
"}",
"internal class FooTest: XCTestCase { " +
"func test1() {}\n " +
"internal func test2() {}\n " +
"public func test3() {}\n " +
"private ↓func test4() {}\n " +
"}",
"public class FooTest: XCTestCase { " +
"func test1() {}\n " +
"internal func test2() {}\n " +
"public func test3() {}\n " +
"private ↓func test4() {}\n " +
"}"
]
)
public func validateFile(
_ file: File,
kind: SwiftDeclarationKind,
dictionary: [String: SourceKitRepresentable])
-> [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, dictionary: dictionary)
guard classViolations.isEmpty else { return classViolations }
let substructure = dictionary["key.substructure"] as? [SourceKitRepresentable] ?? []
return substructure.flatMap { subItem -> [StyleViolation] in
guard
let subDict = subItem as? [String: SourceKitRepresentable],
let kindString = subDict["key.kind"] as? String,
let kind = KindType(rawValue: kindString), kind == .functionMethodInstance
else { return [] }
return self.validateFunction(file, kind: kind, dictionary: subDict)
}
}
fileprivate func isTestClass(_ dictionary: [String: SourceKitRepresentable]) -> Bool {
guard let superclass = superclass(dictionary) else { return false }
let pathMatch = configuration.regex.matches(
in: superclass,
options: [],
range: NSRange(location: 0, length: (superclass as NSString).length))
return !pathMatch.isEmpty
}
fileprivate func validateFunction(
_ file: File,
kind: SwiftDeclarationKind,
dictionary: [String: SourceKitRepresentable])
-> [StyleViolation] {
assert(kind == .functionMethodInstance)
guard
let name = dictionary["key.name"] as? NSString, name.hasPrefix("test")
else { return [] }
return validateAccessControlLevel(file, dictionary: dictionary)
}
fileprivate func validateAccessControlLevel(
_ file: File,
dictionary: [String: SourceKitRepresentable])
-> [StyleViolation] {
guard let acl = AccessControlLevel(dictionary) else { return [] }
if acl.isPrivate {
let offset = Int(dictionary["key.offset"] as? Int64 ?? 0)
return [StyleViolation(
ruleDescription: type(of: self).description,
severity: configuration.severityConfiguration.severity,
location: Location(file: file, byteOffset: offset),
reason: configuration.message)]
}
return []
}
}