Skip to content

Commit 5805e15

Browse files
committed
Correctly identify XCTest-like methods in #if blocks.
The original implementation was only looking for direct members of a `ClassDecl` that were of type `FunctionDecl`, which excluded indirect children nested inside `IfConfigDecl`s.
1 parent 4d1086e commit 5805e15

File tree

2 files changed

+94
-12
lines changed

2 files changed

+94
-12
lines changed

Sources/SwiftFormatRules/AlwaysUseLowerCamelCase.swift

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,9 @@ public final class AlwaysUseLowerCamelCase: SyntaxLintRule {
2929
}
3030

3131
public override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
32-
// Check if this class is an `XCTestCase`, otherwise it cannot contain any test cases.
3332
guard context.importsXCTest == .importsXCTest else { return .visitChildren }
3433

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)
34+
collectTestMethods(from: node.members.members, into: &testCaseFuncs)
4635
return .visitChildren
4736
}
4837

@@ -135,6 +124,33 @@ public final class AlwaysUseLowerCamelCase: SyntaxLintRule {
135124
return .skipChildren
136125
}
137126

127+
/// Collects methods that look like XCTest test case methods from the given member list, inserting
128+
/// them into the given set.
129+
private func collectTestMethods(
130+
from members: MemberDeclListSyntax,
131+
into set: inout Set<FunctionDeclSyntax>
132+
) {
133+
for member in members {
134+
if let ifConfigDecl = member.decl.as(IfConfigDeclSyntax.self) {
135+
// Recurse into any conditional member lists and collect their test methods as well.
136+
for clause in ifConfigDecl.clauses {
137+
if let clauseMembers = clause.elements.as(MemberDeclListSyntax.self) {
138+
collectTestMethods(from: clauseMembers, into: &set)
139+
}
140+
}
141+
} else if let functionDecl = member.decl.as(FunctionDeclSyntax.self) {
142+
// Identify test methods using the same heuristics as XCTest: name starts with "test", has
143+
// no arguments, and returns a void type.
144+
if functionDecl.identifier.text.starts(with: "test")
145+
&& functionDecl.signature.input.parameterList.isEmpty
146+
&& (functionDecl.signature.output.map(\.isVoid) ?? true)
147+
{
148+
set.insert(functionDecl)
149+
}
150+
}
151+
}
152+
}
153+
138154
private func diagnoseLowerCamelCaseViolations(
139155
_ identifier: TokenSyntax, allowUnderscores: Bool, description: String
140156
) {

Tests/SwiftFormatRulesTests/AlwaysUseLowerCamelCaseTests.swift

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,72 @@ final class AlwaysUseLowerCamelCaseTests: LintOrFormatRuleTestCase {
172172
.nameMustBeLowerCamelCase("test_HappyPath_Through_GoodCode_Throws", description: "function"))
173173
}
174174

175+
func testIgnoresUnderscoresInConditionalTestNames() {
176+
let input =
177+
"""
178+
import XCTest
179+
180+
let Test = 1
181+
class UnitTests: XCTestCase {
182+
#if SOME_FEATURE_FLAG
183+
static let My_Constant_Value = 0
184+
func test_HappyPath_Through_GoodCode() {}
185+
private func FooFunc() {}
186+
private func helperFunc_For_HappyPath_Setup() {}
187+
private func testLikeMethod_With_Underscores(_ arg1: ParamType) {}
188+
private func testLikeMethod_With_Underscores2() -> ReturnType {}
189+
func test_HappyPath_Through_GoodCode_ReturnsVoid() -> Void {}
190+
func test_HappyPath_Through_GoodCode_ReturnsShortVoid() -> () {}
191+
func test_HappyPath_Through_GoodCode_Throws() throws {}
192+
#else
193+
func testBadMethod_HasNonVoidReturn() -> ReturnType {}
194+
func testGoodMethod_HasVoidReturn() {}
195+
#if SOME_OTHER_FEATURE_FLAG
196+
func testBadMethod_HasNonVoidReturn2() -> ReturnType {}
197+
func testGoodMethod_HasVoidReturn2() {}
198+
#endif
199+
#endif
200+
}
201+
#endif
202+
"""
203+
performLint(AlwaysUseLowerCamelCase.self, input: input)
204+
XCTAssertDiagnosed(
205+
.nameMustBeLowerCamelCase("Test", description: "constant"), line: 3, column: 5)
206+
XCTAssertDiagnosed(
207+
.nameMustBeLowerCamelCase("My_Constant_Value", description: "constant"), line: 6, column: 16)
208+
XCTAssertNotDiagnosed(
209+
.nameMustBeLowerCamelCase("test_HappyPath_Through_GoodCode", description: "function"))
210+
XCTAssertDiagnosed(
211+
.nameMustBeLowerCamelCase("FooFunc", description: "function"), line: 8, column: 18)
212+
XCTAssertDiagnosed(
213+
.nameMustBeLowerCamelCase("helperFunc_For_HappyPath_Setup", description: "function"),
214+
line: 9, column: 18)
215+
XCTAssertDiagnosed(
216+
.nameMustBeLowerCamelCase("testLikeMethod_With_Underscores", description: "function"),
217+
line: 10, column: 18)
218+
XCTAssertDiagnosed(
219+
.nameMustBeLowerCamelCase("testLikeMethod_With_Underscores2", description: "function"),
220+
line: 11, column: 18)
221+
XCTAssertNotDiagnosed(
222+
.nameMustBeLowerCamelCase(
223+
"test_HappyPath_Through_GoodCode_ReturnsVoid", description: "function"))
224+
XCTAssertNotDiagnosed(
225+
.nameMustBeLowerCamelCase(
226+
"test_HappyPath_Through_GoodCode_ReturnsShortVoid", description: "function"))
227+
XCTAssertNotDiagnosed(
228+
.nameMustBeLowerCamelCase("test_HappyPath_Through_GoodCode_Throws", description: "function"))
229+
XCTAssertDiagnosed(
230+
.nameMustBeLowerCamelCase("testBadMethod_HasNonVoidReturn", description: "function"),
231+
line: 16, column: 10)
232+
XCTAssertNotDiagnosed(
233+
.nameMustBeLowerCamelCase("testGoodMethod_HasVoidReturn", description: "function"))
234+
XCTAssertDiagnosed(
235+
.nameMustBeLowerCamelCase("testBadMethod_HasNonVoidReturn2", description: "function"),
236+
line: 19, column: 12)
237+
XCTAssertNotDiagnosed(
238+
.nameMustBeLowerCamelCase("testGoodMethod_HasVoidReturn2", description: "function"))
239+
}
240+
175241
func testIgnoresFunctionOverrides() {
176242
let input =
177243
"""

0 commit comments

Comments
 (0)