Skip to content

Commit ebdc24c

Browse files
committed
Improve support for XCTAssertEqual and XCTAssertNil in noForceUnwrapInTests (#2208)
1 parent 9fbd4f0 commit ebdc24c

File tree

4 files changed

+164
-14
lines changed

4 files changed

+164
-14
lines changed

Rules.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1684,12 +1684,12 @@ Use XCTUnwrap or #require in test cases, rather than force unwrapping.
16841684
- let myValue = foo.bar!.value as! Value
16851685
- let otherValue = (foo! as! Other).bar
16861686
- otherValue.manager!.prepare()
1687-
- #expect(myValue.property! == other)
1687+
- #expect(myValue!.property! == other)
16881688
+ @Test func myFeature() throws {
16891689
+ let myValue = try #require(foo.bar?.value as? Value)
16901690
+ let otherValue = try #require((foo as? Other)?.bar)
16911691
+ otherValue.manager?.prepare()
1692-
+ #expect(try #require(myValue.property) == other)
1692+
+ #expect(myValue?.property == other)
16931693
}
16941694
}
16951695

@@ -1699,11 +1699,11 @@ Use XCTUnwrap or #require in test cases, rather than force unwrapping.
16991699
- func testMyFeature() {
17001700
- let myValue = foo.bar!.value as! Value
17011701
- let otherValue = (foo! as! Other).bar
1702-
- XCTAssertEqual(myValue.property, "foo")
1702+
- XCTAssertEqual(myValue!.property!, "foo")
17031703
+ func testMyFeature() throws {
17041704
+ let myValue = try XCTUnwrap(foo.bar?.value as? Value)
17051705
+ let otherValue = try XCTUnwrap((foo as? Other)?.bar)
1706-
+ XCTAssertEqual(try XCTUnwrap(myValue.property), otherValue)
1706+
+ XCTAssertEqual(myValue?.property, otherValue)
17071707
}
17081708
}
17091709
```

Sources/Rules/NoForceUnwrapInTests.swift

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,34 @@ public extension FormatRule {
154154
needsUnwrapMethod = false
155155
}
156156

157+
// If this expression is followed by ==, changing `foo!.bar == bar` to `foo?.bar == bar` is a safe change as-is
158+
if let tokenAfterExpression = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: expressionRange.upperBound),
159+
formatter.tokens[tokenAfterExpression] == .operator("==", .infix)
160+
{
161+
needsUnwrapMethod = false
162+
}
163+
164+
// If this expression is within XCTAssertEqual or XCTAssertNil, changing `foo!.bar` to `foo?.bar` is a safe change as-is,
165+
// as long as this isn't a subexpression within a parent operator expression.
166+
if let containingParenScope = formatter.startOfScope(at: expressionRange.lowerBound),
167+
formatter.tokens[containingParenScope] == .startOfScope("("),
168+
let functionNameIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, before: containingParenScope),
169+
formatter.tokens[functionNameIndex].isIdentifier,
170+
let tokenAfterExpression = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: expressionRange.upperBound),
171+
!formatter.tokens[tokenAfterExpression].isOperator
172+
{
173+
let functionName = formatter.tokens[functionNameIndex].string
174+
if functionName == "XCTAssertNil" {
175+
needsUnwrapMethod = false
176+
} else if functionName == "XCTAssertEqual" {
177+
// Ensure this is `XCTAssertEqual(_:_:)`, not `XCTAssertEqual(_:_:accuracy:)` (which doesn't support optionals)
178+
let arguments = formatter.parseFunctionCallArguments(startOfScope: containingParenScope)
179+
if arguments.count == 2 {
180+
needsUnwrapMethod = false
181+
}
182+
}
183+
}
184+
157185
// If this expression is a standalone method call like `foo!.bar()`, then `foo?.bar()` works perfectly well.
158186
// Heuristic: If the scope containing this code is a code block, and the previous token is part of a completely
159187
// separate expression (or, the start of the function body), then this is a standalone expression.
@@ -218,12 +246,12 @@ public extension FormatRule {
218246
- let myValue = foo.bar!.value as! Value
219247
- let otherValue = (foo! as! Other).bar
220248
- otherValue.manager!.prepare()
221-
- #expect(myValue.property! == other)
249+
- #expect(myValue!.property! == other)
222250
+ @Test func myFeature() throws {
223251
+ let myValue = try #require(foo.bar?.value as? Value)
224252
+ let otherValue = try #require((foo as? Other)?.bar)
225253
+ otherValue.manager?.prepare()
226-
+ #expect(try #require(myValue.property) == other)
254+
+ #expect(myValue?.property == other)
227255
}
228256
}
229257
@@ -233,11 +261,11 @@ public extension FormatRule {
233261
- func testMyFeature() {
234262
- let myValue = foo.bar!.value as! Value
235263
- let otherValue = (foo! as! Other).bar
236-
- XCTAssertEqual(myValue.property, "foo")
264+
- XCTAssertEqual(myValue!.property!, "foo")
237265
+ func testMyFeature() throws {
238266
+ let myValue = try XCTUnwrap(foo.bar?.value as? Value)
239267
+ let otherValue = try XCTUnwrap((foo as? Other)?.bar)
240-
+ XCTAssertEqual(try XCTUnwrap(myValue.property), otherValue)
268+
+ XCTAssertEqual(myValue?.property, otherValue)
241269
}
242270
}
243271
```

Tests/CodeOrganizationTests.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ class CodeOrganizationTests: XCTestCase {
106106
// If this is a function call, parse the labels to disambiguate
107107
// between methods with the same base name
108108
var functionCallArguments: [String?]?
109-
if let functionCallStartOfScope = formatter.index(of: .startOfScope("("), after: index) {
109+
if let functionCallStartOfScope = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: index),
110+
formatter.tokens[functionCallStartOfScope] == .startOfScope("(")
111+
{
110112
functionCallArguments = formatter.parseFunctionCallArguments(startOfScope: functionCallStartOfScope).map(\.label)
111113
}
112114

Tests/Rules/NoForceUnwrapInTestsTests.swift

Lines changed: 125 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ final class NoForceUnwrapInTestsTests: XCTestCase {
111111
class TestCase: XCTestCase {
112112
func test_functionCall() throws {
113113
someFunction(try XCTUnwrap(myOptional), try XCTUnwrap(anotherOptional))
114-
XCTAssertEqual(try XCTUnwrap(result?.property), "expected")
114+
XCTAssertEqual(result?.property, "expected")
115115
}
116116
}
117117
"""
@@ -140,7 +140,7 @@ final class NoForceUnwrapInTestsTests: XCTestCase {
140140
func test_ifStatement() throws {
141141
if
142142
try XCTUnwrap(foo?.bar()),
143-
try XCTUnwrap(myOptional?.value) == someValue
143+
myOptional?.value == someValue
144144
{
145145
// do something
146146
}
@@ -198,7 +198,7 @@ final class NoForceUnwrapInTestsTests: XCTestCase {
198198
func test_guardStatement() throws {
199199
guard
200200
try XCTUnwrap(foo?.bar()),
201-
try XCTUnwrap(myOptional?.value) == someValue
201+
myOptional?.value == someValue
202202
else {
203203
return
204204
}
@@ -420,7 +420,7 @@ final class NoForceUnwrapInTestsTests: XCTestCase {
420420
class TestCase: XCTestCase {
421421
func test_complexExpression() throws {
422422
XCTAssertEqual(
423-
try XCTUnwrap(myDictionary["key"]?.processedValue(with: try XCTUnwrap(parameter))),
423+
myDictionary["key"]?.processedValue(with: try XCTUnwrap(parameter)),
424424
expectedResult
425425
)
426426
}
@@ -506,7 +506,7 @@ final class NoForceUnwrapInTestsTests: XCTestCase {
506506
507507
class TestCase: XCTestCase {
508508
func test_forceCasts() throws {
509-
XCTAssertEqual(try XCTUnwrap(route.query as? [String: String]), ["a": "b"])
509+
XCTAssertEqual(route.query as? [String: String], ["a": "b"])
510510
XCTAssert(try XCTUnwrap((foo as? Bar)?.baaz))
511511
XCTAssert(try XCTUnwrap((foo as? Bar)?.baaz))
512512
}
@@ -659,4 +659,124 @@ final class NoForceUnwrapInTestsTests: XCTestCase {
659659
"""
660660
testFormatting(for: input, output, rule: .noForceUnwrapInTests)
661661
}
662+
663+
func testXCTAssertEqual_KeepsForceUnwrapsAsOptionalChaining() throws {
664+
let input = """
665+
import XCTest
666+
667+
class TestCase: XCTestCase {
668+
func test_something() {
669+
XCTAssertEqual(foo!.bar, baaz!.quux)
670+
}
671+
}
672+
"""
673+
let output = """
674+
import XCTest
675+
676+
class TestCase: XCTestCase {
677+
func test_something() {
678+
XCTAssertEqual(foo?.bar, baaz?.quux)
679+
}
680+
}
681+
"""
682+
testFormatting(for: input, output, rule: .noForceUnwrapInTests)
683+
}
684+
685+
func testXCTAssertNil_KeepsForceUnwrapsAsOptionalChaining() throws {
686+
let input = """
687+
import XCTest
688+
689+
class TestCase: XCTestCase {
690+
func test_something() {
691+
XCTAssertNil(foo!.bar)
692+
}
693+
}
694+
"""
695+
let output = """
696+
import XCTest
697+
698+
class TestCase: XCTestCase {
699+
func test_something() {
700+
XCTAssertNil(foo?.bar)
701+
}
702+
}
703+
"""
704+
testFormatting(for: input, output, rule: .noForceUnwrapInTests)
705+
}
706+
707+
func testEqualityComparison_KeepsForceUnwrapsAsOptionalChaining() throws {
708+
let input = """
709+
import Testing
710+
711+
@Test func something() {
712+
#expect(foo!.bar == baaz)
713+
}
714+
"""
715+
let output = """
716+
import Testing
717+
718+
@Test func something() {
719+
#expect(foo?.bar == baaz)
720+
}
721+
"""
722+
testFormatting(for: input, output, rule: .noForceUnwrapInTests)
723+
}
724+
725+
func testEqualityComparisonWithNil_KeepsForceUnwrapsAsOptionalChaining() throws {
726+
let input = """
727+
import Testing
728+
729+
@Test func something() {
730+
#expect(foo!.bar == nil)
731+
}
732+
"""
733+
let output = """
734+
import Testing
735+
736+
@Test func something() {
737+
#expect(foo?.bar == nil)
738+
}
739+
"""
740+
testFormatting(for: input, output, rule: .noForceUnwrapInTests)
741+
}
742+
743+
func testXCTAssertEqualWithAccuracy_RequiresXCTUnwrap() throws {
744+
let input = """
745+
import XCTest
746+
747+
class TestCase: XCTestCase {
748+
func test_something() {
749+
XCTAssertEqual(foo!.value, 3.14, accuracy: 0.01)
750+
}
751+
}
752+
"""
753+
let output = """
754+
import XCTest
755+
756+
class TestCase: XCTestCase {
757+
func test_something() throws {
758+
XCTAssertEqual(try XCTUnwrap(foo?.value), 3.14, accuracy: 0.01)
759+
}
760+
}
761+
"""
762+
testFormatting(for: input, output, rule: .noForceUnwrapInTests)
763+
}
764+
765+
func testForceUnwrapWithOperatorFollowing_RequiresXCTUnwrap() throws {
766+
let input = """
767+
import Testing
768+
769+
@Test func something() {
770+
#expect(foo!.bar + 2 == 3)
771+
}
772+
"""
773+
let output = """
774+
import Testing
775+
776+
@Test func something() throws {
777+
#expect(try #require(foo?.bar) + 2 == 3)
778+
}
779+
"""
780+
testFormatting(for: input, output, rule: .noForceUnwrapInTests)
781+
}
662782
}

0 commit comments

Comments
 (0)