/
AlwaysUseLowerCamelCase.swift
128 lines (115 loc) · 5.15 KB
/
AlwaysUseLowerCamelCase.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
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import SwiftFormatCore
import SwiftSyntax
/// All values should be written in lower camel-case (`lowerCamelCase`).
/// Underscores (except at the beginning of an identifier) are disallowed.
///
/// Lint: If an identifier contains underscores or begins with a capital letter, a lint error is
/// raised.
public final class AlwaysUseLowerCamelCase: SyntaxLintRule {
/// Stores function decls that are test cases.
private var testCaseFuncs = Set<FunctionDeclSyntax>()
public override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind {
// Tracks whether "XCTest" is imported in the source file before processing individual nodes.
setImportsXCTest(context: context, sourceFile: node)
return .visitChildren
}
public override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
// Check if this class is an `XCTestCase`, otherwise it cannot contain any test cases.
guard context.importsXCTest == .importsXCTest else { return .visitChildren }
// Identify and store all of the function decls that are test cases.
let testCases = node.members.members.compactMap {
$0.decl.as(FunctionDeclSyntax.self)
}.filter {
// Filter out non-test methods using the same heuristics as XCTest to identify tests.
// Test methods are methods that start with "test", have no arguments, and void return type.
$0.identifier.text.starts(with: "test")
&& $0.signature.input.parameterList.isEmpty
&& $0.signature.output.map { $0.isVoid } ?? true
}
testCaseFuncs.formUnion(testCases)
return .visitChildren
}
public override func visitPost(_ node: ClassDeclSyntax) {
testCaseFuncs.removeAll()
}
public override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
for binding in node.bindings {
guard let pat = binding.pattern.as(IdentifierPatternSyntax.self) else {
continue
}
diagnoseLowerCamelCaseViolations(
pat.identifier, allowUnderscores: false, description: identifierDescription(for: node))
}
return .skipChildren
}
public override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
// We allow underscores in test names, because there's an existing convention of using
// underscores to separate phrases in very detailed test names.
let allowUnderscores = testCaseFuncs.contains(node)
diagnoseLowerCamelCaseViolations(
node.identifier, allowUnderscores: allowUnderscores,
description: identifierDescription(for: node))
return .skipChildren
}
public override func visit(_ node: EnumCaseElementSyntax) -> SyntaxVisitorContinueKind {
diagnoseLowerCamelCaseViolations(
node.identifier, allowUnderscores: false, description: identifierDescription(for: node))
return .skipChildren
}
private func diagnoseLowerCamelCaseViolations(
_ identifier: TokenSyntax, allowUnderscores: Bool, description: String
) {
guard case .identifier(let text) = identifier.tokenKind else { return }
if text.isEmpty { return }
if (text.dropFirst().contains("_") && !allowUnderscores) || ("A"..."Z").contains(text.first!) {
diagnose(.nameMustBeLowerCamelCase(text, description: description), on: identifier) {
$0.highlight(identifier.sourceRange(converter: self.context.sourceLocationConverter))
}
}
}
}
/// Returns a human readable description of the node type that can be used to describe the
/// identifier of the node in diagnostics from this rule.
///
/// - Parameter node: A node whose identifier may be used in diagnostics.
/// - Returns: A human readable description of the node and its identifier.
fileprivate func identifierDescription<NodeType: SyntaxProtocol>(for node: NodeType) -> String {
switch Syntax(node).as(SyntaxEnum.self) {
case .enumCaseElement: return "enum case"
case .functionDecl: return "function"
case .variableDecl(let variableDecl):
return variableDecl.letOrVarKeyword.tokenKind == .varKeyword ? "variable" : "constant"
default:
return "identifier"
}
}
extension ReturnClauseSyntax {
/// Whether this return clause specifies an explicit `Void` return type.
fileprivate var isVoid: Bool {
if let returnTypeIdentifier = returnType.as(SimpleTypeIdentifierSyntax.self) {
return returnTypeIdentifier.name.text == "Void"
}
if let returnTypeTuple = returnType.as(TupleTypeSyntax.self) {
return returnTypeTuple.elements.isEmpty
}
return false
}
}
extension Diagnostic.Message {
public static func nameMustBeLowerCamelCase(
_ name: String, description: String
) -> Diagnostic.Message {
return .init(.warning, "rename \(description) '\(name)' using lower-camel-case")
}
}