forked from realm/SwiftLint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
PrivateUnitTestRule.swift
152 lines (138 loc) · 6.19 KB
/
PrivateUnitTestRule.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//
// 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.accessibility,
let acl = AccessControlLevel(rawValue: accessibility) else { return nil }
self = acl
}
}
private extension Dictionary where Key: ExpressibleByStringLiteral {
var superclass: String? {
guard let kindString = self.kind,
let kind = SwiftDeclarationKind(rawValue: kindString), kind == .class,
let className = inheritedTypes.first else { return nil }
return className
}
}
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 validate(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: file, dictionary: dictionary)
guard classViolations.isEmpty else { return classViolations }
return dictionary.substructure.flatMap { subDict -> [StyleViolation] in
guard let kindString = subDict.kind,
let kind = KindType(rawValue: kindString), kind == .functionMethodInstance else {
return []
}
return validateFunction(file: file, kind: kind, dictionary: subDict)
}
}
private func isTestClass(_ dictionary: [String: SourceKitRepresentable]) -> Bool {
guard let regex = configuration.regex, let superclass = dictionary.superclass else {
return false
}
let range = NSRange(location: 0, length: superclass.bridge().length)
return !regex.matches(in: superclass, options: [], range: range).isEmpty
}
private func validateFunction(file: File, kind: SwiftDeclarationKind,
dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
assert(kind == .functionMethodInstance)
guard let name = dictionary.name, name.hasPrefix("test") else {
return []
}
return validateAccessControlLevel(file: file, dictionary: dictionary)
}
private func validateAccessControlLevel(file: File,
dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
guard let acl = AccessControlLevel(dictionary), acl.isPrivate else { return [] }
let offset = dictionary.offset ?? 0
return [StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severityConfiguration.severity,
location: Location(file: file, byteOffset: offset),
reason: configuration.message)]
}
}