Skip to content

Commit 4d1086e

Browse files
authored
Merge pull request #417 from allevato/conditional-xctests
Improve `XCTest` import detection logic.
2 parents 3e5f224 + 5c4820f commit 4d1086e

File tree

3 files changed

+128
-15
lines changed

3 files changed

+128
-15
lines changed

Diff for: Sources/SwiftFormatRules/ImportsXCTestVisitor.swift

+23-15
Original file line numberDiff line numberDiff line change
@@ -13,39 +13,47 @@
1313
import SwiftFormatCore
1414
import SwiftSyntax
1515

16-
/// Visitor that determines if the target source file imports XCTest
17-
fileprivate class ImportsXCTestVisitor: SyntaxVisitor {
16+
/// A visitor that determines if the target source file imports `XCTest`.
17+
private class ImportsXCTestVisitor: SyntaxVisitor {
1818
private let context: Context
1919

2020
init(context: Context) {
2121
self.context = context
2222
super.init(viewMode: .sourceAccurate)
2323
}
2424

25-
override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind {
26-
for statement in node.statements {
27-
guard let importDecl = statement.item.as(ImportDeclSyntax.self) else { continue }
28-
for component in importDecl.path {
29-
guard component.name.text == "XCTest" else { continue }
30-
context.importsXCTest = .importsXCTest
31-
return .skipChildren
32-
}
25+
override func visit(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind {
26+
// If we already know whether or not `XCTest` is imported, don't bother doing anything else.
27+
guard context.importsXCTest == .notDetermined else { return .skipChildren }
28+
29+
// If the first import path component is the `XCTest` module, record that fact. Checking in this
30+
// way lets us catch `import XCTest` but also specific decl imports like
31+
// `import class XCTest.XCTestCase`, if someone wants to do that.
32+
if node.path.first!.name.tokenKind == .identifier("XCTest") {
33+
context.importsXCTest = .importsXCTest
3334
}
34-
context.importsXCTest = .doesNotImportXCTest
35+
3536
return .skipChildren
3637
}
38+
39+
override func visitPost(_ node: SourceFileSyntax) {
40+
// If we visited the entire source file and didn't find an `XCTest` import, record that fact.
41+
if context.importsXCTest == .notDetermined {
42+
context.importsXCTest = .doesNotImportXCTest
43+
}
44+
}
3745
}
3846

39-
/// Sets the appropriate value of the importsXCTest field in the Context class, which
40-
/// indicates whether the file contains test code or not.
47+
/// Sets the appropriate value of the `importsXCTest` field in the context, which approximates
48+
/// whether the file contains test code or not.
4149
///
4250
/// This setter will only run the visitor if another rule hasn't already called this function to
43-
/// determine if the source file imports XCTest.
51+
/// determine if the source file imports `XCTest`.
4452
///
4553
/// - Parameters:
4654
/// - context: The context information of the target source file.
4755
/// - sourceFile: The file to be visited.
48-
func setImportsXCTest(context: Context, sourceFile: SourceFileSyntax) {
56+
public func setImportsXCTest(context: Context, sourceFile: SourceFileSyntax) {
4957
guard context.importsXCTest == .notDetermined else { return }
5058
let visitor = ImportsXCTestVisitor(context: context)
5159
visitor.walk(sourceFile)

Diff for: Tests/SwiftFormatRulesTests/AlwaysUseLowerCamelCaseTests.swift

+48
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,54 @@ final class AlwaysUseLowerCamelCaseTests: LintOrFormatRuleTestCase {
124124
.nameMustBeLowerCamelCase("test_HappyPath_Through_GoodCode_Throws", description: "function"))
125125
}
126126

127+
func testIgnoresUnderscoresInTestNamesWhenImportedConditionally() {
128+
let input =
129+
"""
130+
#if SOME_FEATURE_FLAG
131+
import XCTest
132+
133+
let Test = 1
134+
class UnitTests: XCTestCase {
135+
static let My_Constant_Value = 0
136+
func test_HappyPath_Through_GoodCode() {}
137+
private func FooFunc() {}
138+
private func helperFunc_For_HappyPath_Setup() {}
139+
private func testLikeMethod_With_Underscores(_ arg1: ParamType) {}
140+
private func testLikeMethod_With_Underscores2() -> ReturnType {}
141+
func test_HappyPath_Through_GoodCode_ReturnsVoid() -> Void {}
142+
func test_HappyPath_Through_GoodCode_ReturnsShortVoid() -> () {}
143+
func test_HappyPath_Through_GoodCode_Throws() throws {}
144+
}
145+
#endif
146+
"""
147+
performLint(AlwaysUseLowerCamelCase.self, input: input)
148+
XCTAssertDiagnosed(
149+
.nameMustBeLowerCamelCase("Test", description: "constant"), line: 4, column: 7)
150+
XCTAssertDiagnosed(
151+
.nameMustBeLowerCamelCase("My_Constant_Value", description: "constant"), line: 6, column: 16)
152+
XCTAssertNotDiagnosed(
153+
.nameMustBeLowerCamelCase("test_HappyPath_Through_GoodCode", description: "function"))
154+
XCTAssertDiagnosed(
155+
.nameMustBeLowerCamelCase("FooFunc", description: "function"), line: 8, column: 18)
156+
XCTAssertDiagnosed(
157+
.nameMustBeLowerCamelCase("helperFunc_For_HappyPath_Setup", description: "function"),
158+
line: 9, column: 18)
159+
XCTAssertDiagnosed(
160+
.nameMustBeLowerCamelCase("testLikeMethod_With_Underscores", description: "function"),
161+
line: 10, column: 18)
162+
XCTAssertDiagnosed(
163+
.nameMustBeLowerCamelCase("testLikeMethod_With_Underscores2", description: "function"),
164+
line: 11, column: 18)
165+
XCTAssertNotDiagnosed(
166+
.nameMustBeLowerCamelCase(
167+
"test_HappyPath_Through_GoodCode_ReturnsVoid", description: "function"))
168+
XCTAssertNotDiagnosed(
169+
.nameMustBeLowerCamelCase(
170+
"test_HappyPath_Through_GoodCode_ReturnsShortVoid", description: "function"))
171+
XCTAssertNotDiagnosed(
172+
.nameMustBeLowerCamelCase("test_HappyPath_Through_GoodCode_Throws", description: "function"))
173+
}
174+
127175
func testIgnoresFunctionOverrides() {
128176
let input =
129177
"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import SwiftFormatCore
2+
import SwiftFormatRules
3+
import SwiftFormatTestSupport
4+
import SwiftParser
5+
import XCTest
6+
7+
class ImportsXCTestVisitorTests: DiagnosingTestCase {
8+
func testDoesNotImportXCTest() throws {
9+
XCTAssertEqual(
10+
try makeContextAndSetImportsXCTest(source: """
11+
import Foundation
12+
"""),
13+
.doesNotImportXCTest
14+
)
15+
}
16+
17+
func testImportsXCTest() throws {
18+
XCTAssertEqual(
19+
try makeContextAndSetImportsXCTest(source: """
20+
import Foundation
21+
import XCTest
22+
"""),
23+
.importsXCTest
24+
)
25+
}
26+
27+
func testImportsSpecificXCTestDecl() throws {
28+
XCTAssertEqual(
29+
try makeContextAndSetImportsXCTest(source: """
30+
import Foundation
31+
import class XCTest.XCTestCase
32+
"""),
33+
.importsXCTest
34+
)
35+
}
36+
37+
func testImportsXCTestInsideConditional() throws {
38+
XCTAssertEqual(
39+
try makeContextAndSetImportsXCTest(source: """
40+
import Foundation
41+
#if SOME_FEATURE_FLAG
42+
import XCTest
43+
#endif
44+
"""),
45+
.importsXCTest
46+
)
47+
}
48+
49+
/// Parses the given source, makes a new `Context`, then populates and returns its `XCTest`
50+
/// import state.
51+
private func makeContextAndSetImportsXCTest(source: String) throws -> Context.XCTestImportState {
52+
let sourceFile = try Parser.parse(source: source)
53+
let context = makeContext(sourceFileSyntax: sourceFile)
54+
setImportsXCTest(context: context, sourceFile: sourceFile)
55+
return context.importsXCTest
56+
}
57+
}

0 commit comments

Comments
 (0)