Skip to content

Commit ae1ef61

Browse files
authored
Merge pull request #767 from hamtiko/allow-underscore-for-testing-funcs
Add checks for test functions marked with @test attribute in relevant Rules
2 parents fe36876 + d2d5dc3 commit ae1ef61

12 files changed

+139
-1
lines changed

Documentation/RuleDocumentation.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Underscores (except at the beginning of an identifier) are disallowed.
7979

8080
This rule does not apply to test code, defined as code which:
8181
* Contains the line `import XCTest`
82+
* The function is marked with `@Test` attribute
8283

8384
Lint: If an identifier contains underscores or begins with a capital letter, a lint error is
8485
raised.
@@ -188,6 +189,7 @@ Force-unwraps are strongly discouraged and must be documented.
188189

189190
This rule does not apply to test code, defined as code which:
190191
* Contains the line `import XCTest`
192+
* The function is marked with `@Test` attribute
191193

192194
Lint: If a force unwrap is used, a lint warning is raised.
193195

@@ -199,6 +201,7 @@ Force-try (`try!`) is forbidden.
199201

200202
This rule does not apply to test code, defined as code which:
201203
* Contains the line `import XCTest`
204+
* The function is marked with `@Test` attribute
202205

203206
Lint: Using `try!` results in a lint error.
204207

@@ -214,6 +217,7 @@ Certain properties (e.g. `@IBOutlet`) tied to the UI lifecycle are ignored.
214217

215218
This rule does not apply to test code, defined as code which:
216219
* Contains the line `import XCTest`
220+
* The function is marked with `@Test` attribute
217221

218222
TODO: Create exceptions for other UI elements (ex: viewDidLoad)
219223

Sources/SwiftFormat/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ add_library(SwiftFormat
4242
Core/SyntaxLintRule.swift
4343
Core/SyntaxProtocol+Convenience.swift
4444
Core/Trivia+Convenience.swift
45+
Core/WithAttributesSyntax+Convenience.swift
4546
Core/WithSemicolonSyntax.swift
4647
PrettyPrint/Comment.swift
4748
PrettyPrint/Indent+Length.swift

Sources/SwiftFormat/Core/SyntaxProtocol+Convenience.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,19 @@ extension SyntaxProtocol {
149149
}
150150
return leadingTrivia.hasAnyComments
151151
}
152+
153+
/// Indicates whether the node has any function ancestor marked with `@Test` attribute.
154+
var hasTestAncestor: Bool {
155+
var parent = self.parent
156+
while let existingParent = parent {
157+
if let functionDecl = existingParent.as(FunctionDeclSyntax.self),
158+
functionDecl.hasAttribute("Test", inModule: "Testing") {
159+
return true
160+
}
161+
parent = existingParent.parent
162+
}
163+
return false
164+
}
152165
}
153166

154167
extension SyntaxCollection {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
15+
extension WithAttributesSyntax {
16+
/// Indicates whether the node has attribute with the given `name` and `module`.
17+
/// The `module` is only considered if the attribute is written as `@Module.Attribute`.
18+
///
19+
/// - Parameter name: The name of the attribute to lookup.
20+
/// - Parameter module: The module name to lookup the attribute in.
21+
/// - Returns: True if the node has an attribute with the given `name`, otherwise false.
22+
func hasAttribute(_ name: String, inModule module: String) -> Bool {
23+
attributes.contains { attribute in
24+
let attributeName = attribute.as(AttributeSyntax.self)?.attributeName
25+
if let identifier = attributeName?.as(IdentifierTypeSyntax.self) {
26+
// @Attribute syntax
27+
return identifier.name.text == name
28+
}
29+
if let memberType = attributeName?.as(MemberTypeSyntax.self) {
30+
// @Module.Attribute syntax
31+
return memberType.name.text == name
32+
&& memberType.baseType.as(IdentifierTypeSyntax.self)?.name.text == module
33+
}
34+
return false
35+
}
36+
}
37+
}

Sources/SwiftFormat/Rules/AlwaysUseLowerCamelCase.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ public final class AlwaysUseLowerCamelCase: SyntaxLintRule {
100100

101101
// We allow underscores in test names, because there's an existing convention of using
102102
// underscores to separate phrases in very detailed test names.
103-
let allowUnderscores = testCaseFuncs.contains(node)
103+
let allowUnderscores = testCaseFuncs.contains(node) || node.hasAttribute("Test", inModule: "Testing")
104+
104105
diagnoseLowerCamelCaseViolations(
105106
node.name, allowUnderscores: allowUnderscores,
106107
description: identifierDescription(for: node))

Sources/SwiftFormat/Rules/NeverForceUnwrap.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public final class NeverForceUnwrap: SyntaxLintRule {
3434

3535
public override func visit(_ node: ForceUnwrapExprSyntax) -> SyntaxVisitorContinueKind {
3636
guard context.importsXCTest == .doesNotImportXCTest else { return .skipChildren }
37+
// Allow force unwrapping if it is in a function marked with @Test attribute.
38+
if node.hasTestAncestor { return .skipChildren }
3739
diagnose(.doNotForceUnwrap(name: node.expression.trimmedDescription), on: node)
3840
return .skipChildren
3941
}
@@ -44,6 +46,8 @@ public final class NeverForceUnwrap: SyntaxLintRule {
4446
guard context.importsXCTest == .doesNotImportXCTest else { return .skipChildren }
4547
guard let questionOrExclamation = node.questionOrExclamationMark else { return .skipChildren }
4648
guard questionOrExclamation.tokenKind == .exclamationMark else { return .skipChildren }
49+
// Allow force cast if it is in a function marked with @Test attribute.
50+
if node.hasTestAncestor { return .skipChildren }
4751
diagnose(.doNotForceCast(name: node.type.trimmedDescription), on: node)
4852
return .skipChildren
4953
}

Sources/SwiftFormat/Rules/NeverUseForceTry.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public final class NeverUseForceTry: SyntaxLintRule {
3636
public override func visit(_ node: TryExprSyntax) -> SyntaxVisitorContinueKind {
3737
guard context.importsXCTest == .doesNotImportXCTest else { return .skipChildren }
3838
guard let mark = node.questionOrExclamationMark else { return .visitChildren }
39+
// Allow force try if it is in a function marked with @Test attribute.
40+
if node.hasTestAncestor { return .skipChildren }
3941
if mark.tokenKind == .exclamationMark {
4042
diagnose(.doNotForceTry, on: node.tryKeyword)
4143
}

Sources/SwiftFormat/Rules/NeverUseImplicitlyUnwrappedOptionals.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ public final class NeverUseImplicitlyUnwrappedOptionals: SyntaxLintRule {
3939

4040
public override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
4141
guard context.importsXCTest == .doesNotImportXCTest else { return .skipChildren }
42+
// Allow implicitly unwrapping if it is in a function marked with @Test attribute.
43+
if node.hasTestAncestor { return .skipChildren }
4244
// Ignores IBOutlet variables
4345
for attribute in node.attributes {
4446
if (attribute.as(AttributeSyntax.self))?.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "IBOutlet" {

Tests/SwiftFormatTests/Rules/AlwaysUseLowerCamelCaseTests.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,4 +210,27 @@ final class AlwaysUseLowerCamelCaseTests: LintOrFormatRuleTestCase {
210210
]
211211
)
212212
}
213+
214+
func testIgnoresFunctionsWithTestAttributes() {
215+
assertLint(
216+
AlwaysUseLowerCamelCase.self,
217+
"""
218+
@Test
219+
func function_With_Test_Attribute() {}
220+
@Testing.Test("Description for test functions",
221+
.tags(.testTag))
222+
func function_With_Test_Attribute_And_Args() {}
223+
func 1️⃣function_Without_Test_Attribute() {}
224+
@objc
225+
func 2️⃣function_With_Non_Test_Attribute() {}
226+
@Foo.Test
227+
func 3️⃣function_With_Test_Attribute_From_Foo_Module() {}
228+
""",
229+
findings: [
230+
FindingSpec("1️⃣", message: "rename the function 'function_Without_Test_Attribute' using lowerCamelCase"),
231+
FindingSpec("2️⃣", message: "rename the function 'function_With_Non_Test_Attribute' using lowerCamelCase"),
232+
FindingSpec("3️⃣", message: "rename the function 'function_With_Test_Attribute_From_Foo_Module' using lowerCamelCase"),
233+
]
234+
)
235+
}
213236
}

Tests/SwiftFormatTests/Rules/NeverForceUnwrapTests.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,23 @@ final class NeverForceUnwrapTests: LintOrFormatRuleTestCase {
4040
findings: []
4141
)
4242
}
43+
44+
func testIgnoreTestAttributeFunction() {
45+
assertLint(
46+
NeverForceUnwrap.self,
47+
"""
48+
@Test
49+
func testSomeFunc() {
50+
var b = a as! Int
51+
}
52+
@Test
53+
func testAnotherFunc() {
54+
func nestedFunc() {
55+
let c = someValue()!
56+
}
57+
}
58+
""",
59+
findings: []
60+
)
61+
}
4362
}

Tests/SwiftFormatTests/Rules/NeverUseForceTryTests.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,20 @@ final class NeverUseForceTryTests: LintOrFormatRuleTestCase {
3838
findings: []
3939
)
4040
}
41+
42+
func testAllowForceTryInTestAttributeFunction() {
43+
assertLint(
44+
NeverUseForceTry.self,
45+
"""
46+
@Test
47+
func testSomeFunc() {
48+
let document = try! Document(path: "important.data")
49+
func nestedFunc() {
50+
let x = try! someThrowingFunction()
51+
}
52+
}
53+
""",
54+
findings: []
55+
)
56+
}
4157
}

Tests/SwiftFormatTests/Rules/NeverUseImplicitlyUnwrappedOptionalsTests.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,20 @@ final class NeverUseImplicitlyUnwrappedOptionalsTests: LintOrFormatRuleTestCase
3535
findings: []
3636
)
3737
}
38+
39+
func testIgnoreTestAttrinuteFunction() {
40+
assertLint(
41+
NeverUseImplicitlyUnwrappedOptionals.self,
42+
"""
43+
@Test
44+
func testSomeFunc() {
45+
var s: String!
46+
func nestedFunc() {
47+
var f: Foo!
48+
}
49+
}
50+
""",
51+
findings: []
52+
)
53+
}
3854
}

0 commit comments

Comments
 (0)