Skip to content

Commit 4c84a2e

Browse files
committed
Rename throwingTests rule to noForceTryInTests
1 parent d756fd5 commit 4c84a2e

File tree

8 files changed

+130
-106
lines changed

8 files changed

+130
-106
lines changed

Rules.md

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
* [isEmpty](#isEmpty)
110110
* [markTypes](#markTypes)
111111
* [noExplicitOwnership](#noExplicitOwnership)
112+
* [noForceTryInTests](#noForceTryInTests)
112113
* [noForceUnwrapInTests](#noForceUnwrapInTests)
113114
* [noGuardInTests](#noGuardInTests)
114115
* [organizeDeclarations](#organizeDeclarations)
@@ -121,7 +122,6 @@
121122
* [redundantProperty](#redundantProperty)
122123
* [singlePropertyPerLine](#singlePropertyPerLine)
123124
* [sortSwitchCases](#sortSwitchCases)
124-
* [throwingTests](#throwingTests)
125125
* [unusedPrivateDeclarations](#unusedPrivateDeclarations)
126126
* [urlMacro](#urlMacro)
127127
* [wrapConditionalBodies](#wrapConditionalBodies)
@@ -135,6 +135,7 @@
135135
* [sortedImports](#sortedImports)
136136
* [sortedSwitchCases](#sortedSwitchCases)
137137
* [specifiers](#specifiers)
138+
* [throwingTests](#throwingTests)
138139

139140
----------
140141

@@ -1634,6 +1635,38 @@ Don't use explicit ownership modifiers (borrowing / consuming).
16341635
</details>
16351636
<br/>
16361637

1638+
## noForceTryInTests
1639+
1640+
Write tests that use `throws` instead of using `try!`.
1641+
1642+
<details>
1643+
<summary>Examples</summary>
1644+
1645+
```diff
1646+
import Testing
1647+
1648+
struct MyFeatureTests {
1649+
- @Test func doSomething() {
1650+
+ @Test func doSomething() throws {
1651+
- try! MyFeature().doSomething()
1652+
+ try MyFeature().doSomething()
1653+
}
1654+
}
1655+
1656+
import XCTeset
1657+
1658+
class MyFeatureTests: XCTestCase {
1659+
- func test_doSomething() {
1660+
+ func test_doSomething() throws {
1661+
- try! MyFeature().doSomething()
1662+
+ try MyFeature().doSomething()
1663+
}
1664+
}
1665+
```
1666+
1667+
</details>
1668+
<br/>
1669+
16371670
## noForceUnwrapInTests
16381671

16391672
Use XCTUnwrap or #require in test cases, rather than force unwrapping.
@@ -3409,33 +3442,7 @@ In Swift Testing, don't prefix @Test methods with 'test'.
34093442

34103443
Write tests that use `throws` instead of using `try!`.
34113444

3412-
<details>
3413-
<summary>Examples</summary>
3414-
3415-
```diff
3416-
import Testing
3417-
3418-
struct MyFeatureTests {
3419-
- @Test func doSomething() {
3420-
+ @Test func doSomething() throws {
3421-
- try! MyFeature().doSomething()
3422-
+ try MyFeature().doSomething()
3423-
}
3424-
}
3425-
3426-
import XCTeset
3427-
3428-
class MyFeatureTests: XCTestCase {
3429-
- func test_doSomething() {
3430-
+ func test_doSomething() throws {
3431-
- try! MyFeature().doSomething()
3432-
+ try MyFeature().doSomething()
3433-
}
3434-
}
3435-
```
3436-
3437-
</details>
3438-
<br/>
3445+
*Note: throwingTests rule is deprecated. Renamed to `noForceTryInTests`.*
34393446

34403447
## todos
34413448

Sources/RuleRegistry.generated.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ let ruleRegistry: [String: FormatRule] = [
5555
"modifierOrder": .modifierOrder,
5656
"modifiersOnSameLine": .modifiersOnSameLine,
5757
"noExplicitOwnership": .noExplicitOwnership,
58+
"noForceTryInTests": .noForceTryInTests,
5859
"noForceUnwrapInTests": .noForceUnwrapInTests,
5960
"noGuardInTests": .noGuardInTests,
6061
"numberFormatting": .numberFormatting,
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Created by Andy Bartholomew on 5/30/25.
2+
// Copyright © 2025 Airbnb Inc. All rights reserved.
3+
4+
import Foundation
5+
6+
public extension FormatRule {
7+
static let noForceTryInTests = FormatRule(
8+
help: "Write tests that use `throws` instead of using `try!`.",
9+
disabledByDefault: true
10+
) { formatter in
11+
guard let testFramework = formatter.detectTestingFramework() else {
12+
return
13+
}
14+
15+
formatter.forEach(.keyword("func")) { funcKeywordIndex, _ in
16+
guard let functionDecl = formatter.parseFunctionDeclaration(keywordIndex: funcKeywordIndex)
17+
else { return }
18+
19+
switch testFramework {
20+
case .xcTest:
21+
guard functionDecl.name?.starts(with: "test") == true else { return }
22+
case .swiftTesting:
23+
guard formatter.modifiersForDeclaration(at: funcKeywordIndex, contains: "@Test") else { return }
24+
}
25+
26+
guard let bodyRange = functionDecl.bodyRange else { return }
27+
28+
// Find all `try!` and remove the `!`
29+
var foundAnyTryExclamationMarks = false
30+
for index in bodyRange.reversed() {
31+
guard formatter.tokens[index] == .keyword("try") else { continue }
32+
guard let nextTokenIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: index)
33+
else { return }
34+
let nextToken = formatter.tokens[nextTokenIndex]
35+
if nextToken != .operator("!", .postfix) { continue }
36+
37+
// Only remove the `!` if we are not within a closure or nested function,
38+
// where it's not safe to just remove the `!` and make our function throw.
39+
guard formatter.isInFunctionBody(of: functionDecl, at: index) else { continue }
40+
41+
formatter.removeToken(at: nextTokenIndex)
42+
foundAnyTryExclamationMarks = true
43+
}
44+
45+
// If we found any `!`s, add a `throws` if it doesn't already exist.
46+
guard foundAnyTryExclamationMarks else { return }
47+
48+
formatter.addThrowsEffect(to: functionDecl)
49+
}
50+
} examples: {
51+
"""
52+
```diff
53+
import Testing
54+
55+
struct MyFeatureTests {
56+
- @Test func doSomething() {
57+
+ @Test func doSomething() throws {
58+
- try! MyFeature().doSomething()
59+
+ try MyFeature().doSomething()
60+
}
61+
}
62+
63+
import XCTeset
64+
65+
class MyFeatureTests: XCTestCase {
66+
- func test_doSomething() {
67+
+ func test_doSomething() throws {
68+
- try! MyFeature().doSomething()
69+
+ try MyFeature().doSomething()
70+
}
71+
}
72+
```
73+
"""
74+
}
75+
}

Sources/Rules/NoForceUnwrapInTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public extension FormatRule {
77
static let noForceUnwrapInTests = FormatRule(
88
help: "Use XCTUnwrap or #require in test cases, rather than force unwrapping.",
99
disabledByDefault: true,
10-
orderAfter: [.urlMacro, .throwingTests]
10+
orderAfter: [.urlMacro, .noForceTryInTests, .throwingTests]
1111
) { formatter in
1212
guard let testFramework = formatter.detectTestingFramework() else {
1313
return
@@ -57,7 +57,7 @@ public extension FormatRule {
5757
continue
5858
}
5959

60-
// Preserve `try!`s, this is handled separately by the `throwingTests` rule
60+
// Preserve `try!`s, this is handled separately by the `noForceTryInTests` rule
6161
if let previousToken = formatter.index(of: .nonSpaceOrCommentOrLinebreak, before: forceUnwrapOperator),
6262
formatter.tokens[previousToken] == .keyword("try")
6363
{

Sources/Rules/ThrowingTests.swift

Lines changed: 3 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -6,70 +6,11 @@ import Foundation
66
public extension FormatRule {
77
static let throwingTests = FormatRule(
88
help: "Write tests that use `throws` instead of using `try!`.",
9+
deprecationMessage: "Renamed to `noForceTryInTests`.",
910
disabledByDefault: true
1011
) { formatter in
11-
guard let testFramework = formatter.detectTestingFramework() else {
12-
return
13-
}
14-
15-
formatter.forEach(.keyword("func")) { funcKeywordIndex, _ in
16-
guard let functionDecl = formatter.parseFunctionDeclaration(keywordIndex: funcKeywordIndex)
17-
else { return }
18-
19-
switch testFramework {
20-
case .xcTest:
21-
guard functionDecl.name?.starts(with: "test") == true else { return }
22-
case .swiftTesting:
23-
guard formatter.modifiersForDeclaration(at: funcKeywordIndex, contains: "@Test") else { return }
24-
}
25-
26-
guard let bodyRange = functionDecl.bodyRange else { return }
27-
28-
// Find all `try!` and remove the `!`
29-
var foundAnyTryExclamationMarks = false
30-
for index in bodyRange.reversed() {
31-
guard formatter.tokens[index] == .keyword("try") else { continue }
32-
guard let nextTokenIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: index)
33-
else { return }
34-
let nextToken = formatter.tokens[nextTokenIndex]
35-
if nextToken != .operator("!", .postfix) { continue }
36-
37-
// Only remove the `!` if we are not within a closure or nested function,
38-
// where it's not safe to just remove the `!` and make our function throw.
39-
guard formatter.isInFunctionBody(of: functionDecl, at: index) else { continue }
40-
41-
formatter.removeToken(at: nextTokenIndex)
42-
foundAnyTryExclamationMarks = true
43-
}
44-
45-
// If we found any `!`s, add a `throws` if it doesn't already exist.
46-
guard foundAnyTryExclamationMarks else { return }
47-
48-
formatter.addThrowsEffect(to: functionDecl)
49-
}
12+
FormatRule.noForceTryInTests.apply(with: formatter)
5013
} examples: {
51-
"""
52-
```diff
53-
import Testing
54-
55-
struct MyFeatureTests {
56-
- @Test func doSomething() {
57-
+ @Test func doSomething() throws {
58-
- try! MyFeature().doSomething()
59-
+ try MyFeature().doSomething()
60-
}
61-
}
62-
63-
import XCTeset
64-
65-
class MyFeatureTests: XCTestCase {
66-
- func test_doSomething() {
67-
+ func test_doSomething() throws {
68-
- try! MyFeature().doSomething()
69-
+ try MyFeature().doSomething()
70-
}
71-
}
72-
```
73-
"""
14+
nil
7415
}
7516
}

Tests/Rules/ThrowingTestsTests.swift renamed to Tests/Rules/NoForceTryInTestsTests.swift

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import XCTest
55

6-
final class ThrowingTestsTests: XCTestCase {
6+
final class NoForceTryInTestsTests: XCTestCase {
77
func testTestCaseIsUpdated_for_Testing() throws {
88
let input = """
99
import Testing
@@ -19,7 +19,7 @@ final class ThrowingTestsTests: XCTestCase {
1919
try somethingThatThrows()
2020
}
2121
"""
22-
testFormatting(for: input, output, rule: .throwingTests)
22+
testFormatting(for: input, output, rule: .noForceTryInTests)
2323
}
2424

2525
func test_nonTestCaseFunction_IsNotUpdated_for_Testing() throws {
@@ -36,7 +36,7 @@ final class ThrowingTestsTests: XCTestCase {
3636
try! somethingThatThrows()
3737
}
3838
"""
39-
testFormatting(for: input, rule: .throwingTests)
39+
testFormatting(for: input, rule: .noForceTryInTests)
4040
}
4141

4242
func testTestCaseIsUpdated_for_XCTest() throws {
@@ -58,7 +58,7 @@ final class ThrowingTestsTests: XCTestCase {
5858
}
5959
}
6060
"""
61-
testFormatting(for: input, output, rule: .throwingTests)
61+
testFormatting(for: input, output, rule: .noForceTryInTests)
6262
}
6363

6464
func test_nonTestCaseFunction_IsNotUpdated_for_XCTest() throws {
@@ -71,7 +71,7 @@ final class ThrowingTestsTests: XCTestCase {
7171
}
7272
}
7373
"""
74-
testFormatting(for: input, rule: .throwingTests)
74+
testFormatting(for: input, rule: .noForceTryInTests)
7575
}
7676

7777
func testTestCaseIsUpdated_for_async_test() throws {
@@ -89,7 +89,7 @@ final class ThrowingTestsTests: XCTestCase {
8989
try somethingThatThrows()
9090
}
9191
"""
92-
testFormatting(for: input, output, rule: .throwingTests)
92+
testFormatting(for: input, output, rule: .noForceTryInTests)
9393
}
9494

9595
func testTestCaseIsUpdated_for_already_throws() throws {
@@ -107,7 +107,7 @@ final class ThrowingTestsTests: XCTestCase {
107107
try somethingThatThrows()
108108
}
109109
"""
110-
testFormatting(for: input, output, rule: .throwingTests)
110+
testFormatting(for: input, output, rule: .noForceTryInTests)
111111
}
112112

113113
func testTestCaseIsUpdated_for_multiple_try_exclamationMarks() throws {
@@ -127,7 +127,7 @@ final class ThrowingTestsTests: XCTestCase {
127127
try somethingThatThrows()
128128
}
129129
"""
130-
testFormatting(for: input, output, rule: .throwingTests)
130+
testFormatting(for: input, output, rule: .noForceTryInTests)
131131
}
132132

133133
func testTestCaseIsNotUpdated_for_try_exclamationMark_in_closoure() throws {
@@ -140,7 +140,7 @@ final class ThrowingTestsTests: XCTestCase {
140140
}
141141
}
142142
"""
143-
testFormatting(for: input, rule: .throwingTests)
143+
testFormatting(for: input, rule: .noForceTryInTests)
144144
}
145145

146146
func testTestCaseIsUpdated_for_try_exclamationMark_in_if_statement() throws {
@@ -162,7 +162,7 @@ final class ThrowingTestsTests: XCTestCase {
162162
}
163163
}
164164
"""
165-
testFormatting(for: input, output, rule: .throwingTests)
165+
testFormatting(for: input, output, rule: .noForceTryInTests)
166166
}
167167

168168
func testCaseIsNotUpdated_for_try_exclamationMark_in_closure_inside_if_statement() throws {
@@ -177,7 +177,7 @@ final class ThrowingTestsTests: XCTestCase {
177177
}
178178
}
179179
"""
180-
testFormatting(for: input, rule: .throwingTests)
180+
testFormatting(for: input, rule: .noForceTryInTests)
181181
}
182182

183183
func testCaseIsNotUpdated_for_try_exclamationMark_in_closure_inside_nested_function() throws {
@@ -192,6 +192,6 @@ final class ThrowingTestsTests: XCTestCase {
192192
}
193193
}
194194
"""
195-
testFormatting(for: input, rule: .throwingTests)
195+
testFormatting(for: input, rule: .noForceTryInTests)
196196
}
197197
}

Tests/Rules/NoForceUnwrapInTestsTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ final class NoForceUnwrapInTestsTests: XCTestCase {
542542
}
543543
}
544544
"""
545-
testFormatting(for: input, output, rule: .noForceUnwrapInTests, exclude: [.hoistTry, .throwingTests])
545+
testFormatting(for: input, output, rule: .noForceUnwrapInTests, exclude: [.hoistTry, .noForceTryInTests])
546546
}
547547

548548
func testForceUnwrapInAssignmentLHS() {

Tests/Rules/PreferSwiftTestingTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ final class PreferSwiftTestingTests: XCTestCase {
229229
"""
230230

231231
let options = FormatOptions(swiftVersion: "6.0")
232-
testFormatting(for: input, [output], rules: [.preferSwiftTesting, .wrapArguments, .indent, .redundantParens, .hoistTry], options: options, exclude: [.throwingTests])
232+
testFormatting(for: input, [output], rules: [.preferSwiftTesting, .wrapArguments, .indent, .redundantParens, .hoistTry], options: options, exclude: [.noForceTryInTests])
233233
}
234234

235235
func testConvertsMultilineXCTestHelpers() {

0 commit comments

Comments
 (0)