@@ -19,38 +19,84 @@ import SwiftSyntax
1919/// Lint: If an identifier contains underscores or begins with a capital letter, a lint error is
2020/// raised.
2121public 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+ }
2252
2353 public override func visit( _ node: VariableDeclSyntax ) -> SyntaxVisitorContinueKind {
2454 for binding in node. bindings {
2555 guard let pat = binding. pattern. as ( IdentifierPatternSyntax . self) else {
2656 continue
2757 }
28- diagnoseLowerCamelCaseViolations ( pat. identifier)
58+ diagnoseLowerCamelCaseViolations ( pat. identifier, allowUnderscores : false )
2959 }
3060 return . skipChildren
3161 }
3262
3363 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)
3568 return . skipChildren
3669 }
3770
3871 public override func visit( _ node: EnumCaseElementSyntax ) -> SyntaxVisitorContinueKind {
39- diagnoseLowerCamelCaseViolations ( node. identifier)
72+ diagnoseLowerCamelCaseViolations ( node. identifier, allowUnderscores : false )
4073 return . skipChildren
4174 }
4275
43- private func diagnoseLowerCamelCaseViolations( _ identifier: TokenSyntax ) {
76+ private func diagnoseLowerCamelCaseViolations( _ identifier: TokenSyntax , allowUnderscores : Bool ) {
4477 guard case . identifier( let text) = identifier. tokenKind else { return }
4578 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!) {
4780 diagnose ( . variableNameMustBeLowerCamelCase( text) , on: identifier) {
4881 $0. highlight ( identifier. sourceRange ( converter: self . context. sourceLocationConverter) )
4982 }
5083 }
5184 }
5285}
5386
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+
54100extension Diagnostic . Message {
55101 public static func variableNameMustBeLowerCamelCase( _ name: String ) -> Diagnostic . Message {
56102 return . init( . warning, " rename variable ' \( name) ' using lower-camel-case " )
0 commit comments