File: AlwaysUseLowerCamelCase.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (202 lines) | stat: -rw-r--r-- 8,548 bytes parent folder | download
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"
  }
}