@@ -19,38 +19,84 @@ import SwiftSyntax
19
19
/// Lint: If an identifier contains underscores or begins with a capital letter, a lint error is
20
20
/// raised.
21
21
public final class AlwaysUseLowerCamelCase : SyntaxLintRule {
22
+ /// Stores function decls that are test cases.
23
+ private var testCaseFuncs = Set < FunctionDeclSyntax > ( )
24
+
25
+ public override func visit( _ node: SourceFileSyntax ) -> SyntaxVisitorContinueKind {
26
+ // Tracks whether "XCTest" is imported in the source file before processing individual nodes.
27
+ setImportsXCTest ( context: context, sourceFile: node)
28
+ return . visitChildren
29
+ }
30
+
31
+ public override func visit( _ node: ClassDeclSyntax ) -> SyntaxVisitorContinueKind {
32
+ // Check if this class is an `XCTestCase`, otherwise it cannot contain any test cases.
33
+ guard context. importsXCTest == . importsXCTest else { return . visitChildren }
34
+
35
+ // Identify and store all of the function decls that are test cases.
36
+ let testCases = node. members. members. compactMap {
37
+ $0. decl. as ( FunctionDeclSyntax . self)
38
+ } . filter {
39
+ // Filter out non-test methods using the same heuristics as XCTest to identify tests.
40
+ // Test methods are methods that start with "test", have no arguments, and void return type.
41
+ $0. identifier. text. starts ( with: " test " )
42
+ && $0. signature. input. parameterList. isEmpty
43
+ && $0. signature. output. map { $0. isVoid } ?? true
44
+ }
45
+ testCaseFuncs. formUnion ( testCases)
46
+ return . visitChildren
47
+ }
48
+
49
+ public override func visitPost( _ node: ClassDeclSyntax ) {
50
+ testCaseFuncs. removeAll ( )
51
+ }
22
52
23
53
public override func visit( _ node: VariableDeclSyntax ) -> SyntaxVisitorContinueKind {
24
54
for binding in node. bindings {
25
55
guard let pat = binding. pattern. as ( IdentifierPatternSyntax . self) else {
26
56
continue
27
57
}
28
- diagnoseLowerCamelCaseViolations ( pat. identifier)
58
+ diagnoseLowerCamelCaseViolations ( pat. identifier, allowUnderscores : false )
29
59
}
30
60
return . skipChildren
31
61
}
32
62
33
63
public override func visit( _ node: FunctionDeclSyntax ) -> SyntaxVisitorContinueKind {
34
- diagnoseLowerCamelCaseViolations ( node. identifier)
64
+ // We allow underscores in test names, because there's an existing convention of using
65
+ // underscores to separate phrases in very detailed test names.
66
+ let allowUnderscores = testCaseFuncs. contains ( node)
67
+ diagnoseLowerCamelCaseViolations ( node. identifier, allowUnderscores: allowUnderscores)
35
68
return . skipChildren
36
69
}
37
70
38
71
public override func visit( _ node: EnumCaseElementSyntax ) -> SyntaxVisitorContinueKind {
39
- diagnoseLowerCamelCaseViolations ( node. identifier)
72
+ diagnoseLowerCamelCaseViolations ( node. identifier, allowUnderscores : false )
40
73
return . skipChildren
41
74
}
42
75
43
- private func diagnoseLowerCamelCaseViolations( _ identifier: TokenSyntax ) {
76
+ private func diagnoseLowerCamelCaseViolations( _ identifier: TokenSyntax , allowUnderscores : Bool ) {
44
77
guard case . identifier( let text) = identifier. tokenKind else { return }
45
78
if text. isEmpty { return }
46
- if text. dropFirst ( ) . contains ( " _ " ) || ( " A " ... " Z " ) . contains ( text. first!) {
79
+ if ( text. dropFirst ( ) . contains ( " _ " ) && !allowUnderscores ) || ( " A " ... " Z " ) . contains ( text. first!) {
47
80
diagnose ( . variableNameMustBeLowerCamelCase( text) , on: identifier) {
48
81
$0. highlight ( identifier. sourceRange ( converter: self . context. sourceLocationConverter) )
49
82
}
50
83
}
51
84
}
52
85
}
53
86
87
+ extension ReturnClauseSyntax {
88
+ /// Whether this return clause specifies an explicit `Void` return type.
89
+ fileprivate var isVoid : Bool {
90
+ if let returnTypeIdentifier = returnType. as ( SimpleTypeIdentifierSyntax . self) {
91
+ return returnTypeIdentifier. name. text == " Void "
92
+ }
93
+ if let returnTypeTuple = returnType. as ( TupleTypeSyntax . self) {
94
+ return returnTypeTuple. elements. isEmpty
95
+ }
96
+ return false
97
+ }
98
+ }
99
+
54
100
extension Diagnostic . Message {
55
101
public static func variableNameMustBeLowerCamelCase( _ name: String ) -> Diagnostic . Message {
56
102
return . init( . warning, " rename variable ' \( name) ' using lower-camel-case " )
0 commit comments