Skip to content

Commit 9080762

Browse files
committed
Merge pull request #221 from dylansturg/test_name_freedom
Add an exception to AlwaysUseLowerCamelCase for test names.
1 parent ea02880 commit 9080762

File tree

3 files changed

+102
-9
lines changed

3 files changed

+102
-9
lines changed

Sources/SwiftFormatRules/AlwaysUseLowerCamelCase.swift

+51-5
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

+1-1
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
}
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)