Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -1668,6 +1668,7 @@ Convert guard statements and trailing if statements in unit tests to
Option | Description
--- | ---
`--guard-like-if-statements` | Convert guard-like trailing if statements in tests: "convert" or "preserve" (default)
`--boolean-guards-in-tests` | Boolean guards: "preserve-xctest" (default), "preserve" or "convert"

<details>
<summary>Examples</summary>
Expand Down
7 changes: 7 additions & 0 deletions Sources/OptionDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1509,6 +1509,13 @@ struct _Descriptors {
falseValues: ["preserve"]
)

let booleanGuardsInTests = OptionDescriptor(
argumentName: "boolean-guards-in-tests",
displayName: "Boolean Guards In Tests",
help: "Boolean guards: \"preserve-xctest\" (default), \"preserve\" or \"convert\"",
keyPath: \.booleanGuardsInTests
)

let redundantOptionalBinding = OptionDescriptor(
argumentName: "redundant-optional-binding",
displayName: "Redundant Optional Binding",
Expand Down
14 changes: 14 additions & 0 deletions Sources/Options.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,17 @@ public enum RedundantOptionalBindingMode: String, CaseIterable {
case always
}

/// How the noGuardInTests rule handles guards with boolean conditions
public enum BooleanGuardsInTests: String, CaseIterable {
/// Convert boolean guards in every test framework, the behaviour before this option existed
case convert
/// Preserve boolean guards in every test framework
case preserve
/// Preserve boolean guards under XCTest, where `XCTAssert` does not halt the test,
/// and convert them under Swift Testing, where `try #require` throws and so keeps the early exit
case preserveXCTest = "preserve-xctest"
}

public enum TrailingCommas: String, CaseIterable {
case never
case always
Expand Down Expand Up @@ -934,6 +945,7 @@ public struct FormatOptions: CustomStringConvertible {
public var suiteNameFormat: SwiftTestingNameFormat
public var testCaseAccessControl: Visibility
public var guardLikeIfStatements: Bool
public var booleanGuardsInTests: BooleanGuardsInTests
public var redundantOptionalBinding: RedundantOptionalBindingMode
public var singleLineTernary: SingleLineTernary

Expand Down Expand Up @@ -1087,6 +1099,7 @@ public struct FormatOptions: CustomStringConvertible {
suiteNameFormat: SwiftTestingNameFormat = .preserve,
testCaseAccessControl: Visibility = .internal,
guardLikeIfStatements: Bool = false,
booleanGuardsInTests: BooleanGuardsInTests = .preserveXCTest,
redundantOptionalBinding: RedundantOptionalBindingMode = .sameNameOnly,
singleLineTernary: SingleLineTernary = .convert,
// Doesn't really belong here, but hard to put elsewhere
Expand Down Expand Up @@ -1229,6 +1242,7 @@ public struct FormatOptions: CustomStringConvertible {
self.suiteNameFormat = suiteNameFormat
self.testCaseAccessControl = testCaseAccessControl
self.guardLikeIfStatements = guardLikeIfStatements
self.booleanGuardsInTests = booleanGuardsInTests
self.redundantOptionalBinding = redundantOptionalBinding
self.singleLineTernary = singleLineTernary
self.indentComments = indentComments
Expand Down
19 changes: 18 additions & 1 deletion Sources/Rules/NoGuardInTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public extension FormatRule {
`try #require(...)` or `try XCTUnwrap(...)` / `XCTAssert(...)`.
""",
disabledByDefault: true,
options: ["guard-like-if-statements"],
options: ["guard-like-if-statements", "boolean-guards-in-tests"],
sharedOptions: ["linebreaks"]
) { formatter in
guard let testFramework = formatter.detectTestingFramework() else {
Expand Down Expand Up @@ -172,6 +172,23 @@ public extension FormatRule {
// Skip if #available / #unavailable (can't be converted to #expect)
return true
case .booleanExpression:
// Converting a boolean guard discards its early exit whenever the
// replacement does not halt, so code after the guard would run with
// the condition false. That is true of `XCTAssert` but not of
// `try #require`, which throws, so the two frameworks differ.
if isGuard {
switch formatter.options.booleanGuardsInTests {
case .preserve:
return true
case .preserveXCTest:
if testFramework == .xcTest {
return true
}
case .convert:
break
}
}

// XCTAssert doesn't halt the test, so we can't use it to replace
// if statement conditions. For guard statements, XCTAssert is fine
// since the guard else block handles early exit.
Expand Down
130 changes: 120 additions & 10 deletions Tests/Rules/NoGuardInTestsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ final class NoGuardInTestsTests: XCTestCase {
}
}
"""
testFormatting(for: input, output, rule: .noGuardInTests, exclude: [.blankLinesAfterGuardStatements])
testFormatting(for: input, output, rule: .noGuardInTests, options: FormatOptions(booleanGuardsInTests: .convert), exclude: [.blankLinesAfterGuardStatements])
}

func testPreserveFailMessageWithInterpolations() {
Expand All @@ -317,7 +317,7 @@ final class NoGuardInTestsTests: XCTestCase {
}
}
"""
testFormatting(for: input, output, rule: .noGuardInTests, exclude: [.blankLinesAfterGuardStatements])
testFormatting(for: input, output, rule: .noGuardInTests, options: FormatOptions(booleanGuardsInTests: .convert), exclude: [.blankLinesAfterGuardStatements])
}

func testNoMangleNontrivialGuardBody() {
Expand Down Expand Up @@ -388,7 +388,7 @@ final class NoGuardInTestsTests: XCTestCase {
}
}
"""
testFormatting(for: input, output, rule: .noGuardInTests)
testFormatting(for: input, output, rule: .noGuardInTests, options: FormatOptions(booleanGuardsInTests: .convert))
}

func testReplaceMultipleGuardConditionsWithMixedPatterns() {
Expand Down Expand Up @@ -417,7 +417,7 @@ final class NoGuardInTestsTests: XCTestCase {
}
}
"""
testFormatting(for: input, output, rule: .noGuardInTests)
testFormatting(for: input, output, rule: .noGuardInTests, options: FormatOptions(booleanGuardsInTests: .convert))
}

func testSimpleMultipleConditions() {
Expand All @@ -443,7 +443,7 @@ final class NoGuardInTestsTests: XCTestCase {
}
}
"""
testFormatting(for: input, output, rule: .noGuardInTests)
testFormatting(for: input, output, rule: .noGuardInTests, options: FormatOptions(booleanGuardsInTests: .convert))
}

func testSimpleMultipleConditions2() {
Expand All @@ -469,7 +469,7 @@ final class NoGuardInTestsTests: XCTestCase {
}
}
"""
testFormatting(for: input, output, rule: .noGuardInTests, exclude: [.wrapConditionalBodies])
testFormatting(for: input, output, rule: .noGuardInTests, options: FormatOptions(booleanGuardsInTests: .convert), exclude: [.wrapConditionalBodies])
}

func testHandlesFiveConditions() {
Expand Down Expand Up @@ -594,7 +594,7 @@ final class NoGuardInTestsTests: XCTestCase {
}
}
"""
testFormatting(for: input, output, rule: .noGuardInTests, exclude: [.wrapMultilineStatementBraces, .elseOnSameLine, .blankLinesAfterGuardStatements, .wrapArguments])
testFormatting(for: input, output, rule: .noGuardInTests, options: FormatOptions(booleanGuardsInTests: .convert), exclude: [.wrapMultilineStatementBraces, .elseOnSameLine, .blankLinesAfterGuardStatements, .wrapArguments])
}

// MARK: - Swift Testing tests
Expand Down Expand Up @@ -922,7 +922,7 @@ final class NoGuardInTestsTests: XCTestCase {
}
}
"""
testFormatting(for: input, output, rule: .noGuardInTests)
testFormatting(for: input, output, rule: .noGuardInTests, options: FormatOptions(booleanGuardsInTests: .convert))
}

// MARK: - Variable shadowing tests
Expand Down Expand Up @@ -1174,7 +1174,7 @@ final class NoGuardInTestsTests: XCTestCase {
}
}
"""
testFormatting(for: input, output, rule: .noGuardInTests, exclude: [.blankLinesAfterGuardStatements, .unusedArguments])
testFormatting(for: input, output, rule: .noGuardInTests, options: FormatOptions(booleanGuardsInTests: .convert), exclude: [.blankLinesAfterGuardStatements, .unusedArguments])
}

func testConvertsBooleanConditionsToRequire() {
Expand Down Expand Up @@ -1235,7 +1235,117 @@ final class NoGuardInTestsTests: XCTestCase {
}
}
"""
testFormatting(for: input, output, rule: .noGuardInTests)
testFormatting(for: input, output, rule: .noGuardInTests, options: FormatOptions(booleanGuardsInTests: .convert))
}

func testPreservesBooleanGuardWithOptionDisabled() {
let input = """
import XCTest

class TestCase: XCTestCase {
func test_something() {
guard polygons.count > 1 else {
return
}

let a = Set(polygons[0].vertices)
}
}
"""
let options = FormatOptions(booleanGuardsInTests: .preserve)
testFormatting(for: input, rule: .noGuardInTests, options: options, exclude: [.blankLinesAfterGuardStatements])
}

func testPreservesBooleanGuardWithOptionDisabledSwiftTesting() {
let input = """
import Testing

struct SomeTests {
@Test
func something() {
guard polygons.count > 1 else {
return
}

let a = Set(polygons[0].vertices)
}
}
"""
let options = FormatOptions(booleanGuardsInTests: .preserve)
testFormatting(for: input, rule: .noGuardInTests, options: options, exclude: [.blankLinesAfterGuardStatements])
}

func testPreservesGuardWithMixedConditionsWithOptionDisabled() {
let input = """
import XCTest

class TestCase: XCTestCase {
func test_something() {
guard someCondition,
let value = optionalValue else {
XCTFail()
return
}
print(value)
}
}
"""
let options = FormatOptions(booleanGuardsInTests: .preserve)
testFormatting(for: input, rule: .noGuardInTests, options: options, exclude: [.blankLinesAfterGuardStatements, .unusedArguments, .elseOnSameLine, .wrapMultilineStatementBraces])
}

func testStillConvertsOptionalBindingGuardWithOptionDisabled() {
let input = """
import XCTest

class TestCase: XCTestCase {
func test_something() {
guard let value = optionalValue else {
XCTFail()
return
}
print(value)
}
}
"""
let output = """
import XCTest

class TestCase: XCTestCase {
func test_something() throws {
let value = try XCTUnwrap(optionalValue)
print(value)
}
}
"""
let options = FormatOptions(booleanGuardsInTests: .preserve)
testFormatting(for: input, output, rule: .noGuardInTests, options: options, exclude: [.blankLinesAfterGuardStatements, .unusedArguments])
}

func testStillConvertsTrailingIfWithOptionDisabled() {
let input = """
import XCTest

class TestCase: XCTestCase {
func test_something() {
if let value = optionalValue {
print(value)
}
}
}
"""
let output = """
import XCTest

class TestCase: XCTestCase {
func test_something() throws {
let value = try XCTUnwrap(optionalValue)
print(value)
}
}
"""
let options = FormatOptions(guardLikeIfStatements: true, booleanGuardsInTests: .preserve)
testFormatting(for: input, [output], rules: [.noGuardInTests, .indent], options: options)
}

func testPreservesGuardWithShadowedVariable() {
Expand Down