Skip to content

Commit 856ff83

Browse files
committed
Add an exception to AlwaysUseLowerCamelCase for test names.
When using descriptive names, e.g. explaining the context and expected outcome in the name, the test names can be quite long and using underscores in the names to organize phrases becomes beneficial. In other situations (i.e. not test cases), it's better to use shorter names that don't require such organization. This change adds an exception specifically for test cases so that the linter won't complain about underscores in those names.
1 parent b118f62 commit 856ff83

File tree

3 files changed

+102
-9
lines changed

3 files changed

+102
-9
lines changed

Sources/SwiftFormatRules/AlwaysUseLowerCamelCase.swift

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
2121
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+
}
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+
54100
extension Diagnostic.Message {
55101
public static func variableNameMustBeLowerCamelCase(_ name: String) -> Diagnostic.Message {
56102
return .init(.warning, "rename variable '\(name)' using lower-camel-case")

Sources/SwiftFormatRules/NeverForceUnwrap.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import SwiftSyntax
1919
public final class NeverForceUnwrap: SyntaxLintRule {
2020

2121
public override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind {
22-
// Tracks whether "XCTest" is imported in the source file before processing the individual
22+
// Tracks whether "XCTest" is imported in the source file before processing individual nodes.
2323
setImportsXCTest(context: context, sourceFile: node)
2424
return .visitChildren
2525
}
Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import SwiftFormatRules
22

33
final class AlwaysUseLowerCamelCaseTests: LintOrFormatRuleTestCase {
4+
override func setUp() {
5+
super.setUp()
6+
shouldCheckForUnassertedDiagnostics = true
7+
}
8+
49
func testInvalidVariableCasing() {
510
let input =
611
"""
@@ -11,13 +16,55 @@ final class AlwaysUseLowerCamelCaseTests: LintOrFormatRuleTestCase {
1116
struct Foo {
1217
func FooFunc() {}
1318
}
19+
class UnitTests: XCTestCase {
20+
func test_HappyPath_Through_GoodCode() {}
21+
}
1422
"""
1523
performLint(AlwaysUseLowerCamelCase.self, input: input)
16-
XCTAssertDiagnosed(.variableNameMustBeLowerCamelCase("Test"))
24+
XCTAssertDiagnosed(.variableNameMustBeLowerCamelCase("Test"), line: 1, column: 5)
1725
XCTAssertNotDiagnosed(.variableNameMustBeLowerCamelCase("foo"))
18-
XCTAssertDiagnosed(.variableNameMustBeLowerCamelCase("bad_name"))
26+
XCTAssertDiagnosed(.variableNameMustBeLowerCamelCase("bad_name"), line: 3, column: 5)
1927
XCTAssertNotDiagnosed(.variableNameMustBeLowerCamelCase("_okayName"))
2028
XCTAssertNotDiagnosed(.variableNameMustBeLowerCamelCase("Foo"))
21-
XCTAssertDiagnosed(.variableNameMustBeLowerCamelCase("FooFunc"))
29+
XCTAssertDiagnosed(.variableNameMustBeLowerCamelCase("FooFunc"), line: 6, column: 8)
30+
XCTAssertDiagnosed(
31+
.variableNameMustBeLowerCamelCase("test_HappyPath_Through_GoodCode"), line: 9, column: 8)
32+
}
33+
34+
func testIgnoresUnderscoresInTestNames() {
35+
let input =
36+
"""
37+
import XCTest
38+
39+
let Test = 1
40+
class UnitTests: XCTestCase {
41+
static let My_Constant_Value = 0
42+
func test_HappyPath_Through_GoodCode() {}
43+
private func FooFunc() {}
44+
private func helperFunc_For_HappyPath_Setup() {}
45+
private func testLikeMethod_With_Underscores(_ arg1: ParamType) {}
46+
private func testLikeMethod_With_Underscores2() -> ReturnType {}
47+
func test_HappyPath_Through_GoodCode_ReturnsVoid() -> Void {}
48+
func test_HappyPath_Through_GoodCode_ReturnsShortVoid() -> () {}
49+
func test_HappyPath_Through_GoodCode_Throws() throws {}
50+
}
51+
"""
52+
performLint(AlwaysUseLowerCamelCase.self, input: input)
53+
XCTAssertDiagnosed(.variableNameMustBeLowerCamelCase("Test"), line: 3, column: 5)
54+
XCTAssertDiagnosed(.variableNameMustBeLowerCamelCase("My_Constant_Value"), line: 5, column: 14)
55+
XCTAssertNotDiagnosed(.variableNameMustBeLowerCamelCase("test_HappyPath_Through_GoodCode"))
56+
XCTAssertDiagnosed(.variableNameMustBeLowerCamelCase("FooFunc"), line: 7, column: 16)
57+
XCTAssertDiagnosed(
58+
.variableNameMustBeLowerCamelCase("helperFunc_For_HappyPath_Setup"), line: 8, column: 16)
59+
XCTAssertDiagnosed(
60+
.variableNameMustBeLowerCamelCase("testLikeMethod_With_Underscores"), line: 9, column: 16)
61+
XCTAssertDiagnosed(
62+
.variableNameMustBeLowerCamelCase("testLikeMethod_With_Underscores2"), line: 10, column: 16)
63+
XCTAssertNotDiagnosed(
64+
.variableNameMustBeLowerCamelCase("test_HappyPath_Through_GoodCode_ReturnsVoid"))
65+
XCTAssertNotDiagnosed(
66+
.variableNameMustBeLowerCamelCase("test_HappyPath_Through_GoodCode_ReturnsShortVoid"))
67+
XCTAssertNotDiagnosed(
68+
.variableNameMustBeLowerCamelCase("test_HappyPath_Through_GoodCode_Throws"))
2269
}
2370
}

0 commit comments

Comments
 (0)