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 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
|
//===----------------------------------------------------------------------===//
//
// 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 SwiftSyntax
/// All values should be written in lower camel-case (`lowerCamelCase`).
/// Underscores (except at the beginning of an identifier) are disallowed.
///
/// This rule does not apply to test code, defined as code which:
/// * Contains the line `import XCTest`
///
/// Lint: If an identifier contains underscores or begins with a capital letter, a lint error is
/// raised.
@_spi(Rules)
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 {
guard context.importsXCTest == .importsXCTest else { return .visitChildren }
collectTestMethods(from: node.memberBlock.members, into: &testCaseFuncs)
return .visitChildren
}
public override func visitPost(_ node: ClassDeclSyntax) {
testCaseFuncs.removeAll()
}
public override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
// Don't diagnose any issues when the variable is overriding, because this declaration can't
// rename the variable. If the user analyzes the code where the variable is really declared,
// then the diagnostic can be raised for just that location.
if node.modifiers.contains(anyOf: [.override]) {
return .visitChildren
}
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 .visitChildren
}
public override func visit(_ node: OptionalBindingConditionSyntax) -> SyntaxVisitorContinueKind {
guard let pattern = node.pattern.as(IdentifierPatternSyntax.self) else {
return .visitChildren
}
diagnoseLowerCamelCaseViolations(
pattern.identifier, allowUnderscores: false, description: identifierDescription(for: node))
return .visitChildren
}
public override func visit(_ node: ClosureSignatureSyntax) -> SyntaxVisitorContinueKind {
if let input = node.parameterClause {
if let closureParamList = input.as(ClosureShorthandParameterListSyntax.self) {
for param in closureParamList {
diagnoseLowerCamelCaseViolations(
param.name, allowUnderscores: false, description: identifierDescription(for: node))
}
} else if let parameterClause = input.as(ClosureParameterClauseSyntax.self) {
for param in parameterClause.parameters {
diagnoseLowerCamelCaseViolations(
param.firstName, allowUnderscores: false, description: identifierDescription(for: node))
if let secondName = param.secondName {
diagnoseLowerCamelCaseViolations(
secondName, allowUnderscores: false, description: identifierDescription(for: node))
}
}
}
}
return .visitChildren
}
public override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
// Don't diagnose any issues when the function is overriding, because this declaration can't
// rename the function. If the user analyzes the code where the function is really declared,
// then the diagnostic can be raised for just that location.
if node.modifiers.contains(anyOf: [.override]) {
return .visitChildren
}
// 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.name, allowUnderscores: allowUnderscores,
description: identifierDescription(for: node))
for param in node.signature.parameterClause.parameters {
// These identifiers aren't described using `identifierDescription(for:)` because no single
// node can disambiguate the argument label from the parameter name.
diagnoseLowerCamelCaseViolations(
param.firstName, allowUnderscores: false, description: "argument label")
if let paramName = param.secondName {
diagnoseLowerCamelCaseViolations(
paramName, allowUnderscores: false, description: "function parameter")
}
}
return .visitChildren
}
public override func visit(_ node: EnumCaseElementSyntax) -> SyntaxVisitorContinueKind {
diagnoseLowerCamelCaseViolations(
node.name, allowUnderscores: false, description: identifierDescription(for: node))
return .skipChildren
}
/// Collects methods that look like XCTest test case methods from the given member list, inserting
/// them into the given set.
private func collectTestMethods(
from members: MemberBlockItemListSyntax,
into set: inout Set<FunctionDeclSyntax>
) {
for member in members {
if let ifConfigDecl = member.decl.as(IfConfigDeclSyntax.self) {
// Recurse into any conditional member lists and collect their test methods as well.
for clause in ifConfigDecl.clauses {
if let clauseMembers = clause.elements?.as(MemberBlockItemListSyntax.self) {
collectTestMethods(from: clauseMembers, into: &set)
}
}
} else if let functionDecl = member.decl.as(FunctionDeclSyntax.self) {
// Identify test methods using the same heuristics as XCTest: name starts with "test", has
// no arguments, and returns a void type.
if functionDecl.name.text.starts(with: "test")
&& functionDecl.signature.parameterClause.parameters.isEmpty
&& (functionDecl.signature.returnClause.map(\.isVoid) ?? true)
{
set.insert(functionDecl)
}
}
}
}
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)
}
}
}
/// 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 .closureSignature: return "closure parameter"
case .enumCaseElement: return "enum case"
case .functionDecl: return "function"
case .optionalBindingCondition(let binding):
return binding.bindingSpecifier.tokenKind == .keyword(.var) ? "variable" : "constant"
case .variableDecl(let variableDecl):
return variableDecl.bindingSpecifier.tokenKind == .keyword(.var) ? "variable" : "constant"
default:
return "identifier"
}
}
extension ReturnClauseSyntax {
/// Whether this return clause specifies an explicit `Void` return type.
fileprivate var isVoid: Bool {
if let returnTypeIdentifier = type.as(IdentifierTypeSyntax.self) {
return returnTypeIdentifier.name.text == "Void"
}
if let returnTypeTuple = type.as(TupleTypeSyntax.self) {
return returnTypeTuple.elements.isEmpty
}
return false
}
}
extension Finding.Message {
fileprivate static func nameMustBeLowerCamelCase(
_ name: String, description: String
) -> Finding.Message {
"rename the \(description) '\(name)' using lowerCamelCase"
}
}
|