mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
619 lines
26 KiB
Swift
619 lines
26 KiB
Swift
//
|
|
// PreferSwiftTesting.swift
|
|
// SwiftFormatTests
|
|
//
|
|
// Created by Cal Stephens on 1/25/25.
|
|
// Copyright © 2025 Nick Lockwood. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
public extension FormatRule {
|
|
static let preferSwiftTesting = FormatRule(
|
|
help: "Prefer the Swift Testing library over XCTest.",
|
|
disabledByDefault: true,
|
|
options: ["xctest-symbols", "default-test-suite-attributes"]
|
|
) { formatter in
|
|
// Swift Testing was introduced in Xcode 16.0 with Swift 6.0
|
|
guard formatter.options.swiftVersion >= "6.0" else { return }
|
|
|
|
// Ensure there are no XCTest helpers that this rule doesn't support
|
|
// before we start converting any test cases.
|
|
guard !formatter.hasUnsupportedXCTestHelper() else { return }
|
|
|
|
let declarations = formatter.parseDeclarations()
|
|
|
|
let xcTestSuites = declarations
|
|
.compactMap(\.asTypeDeclaration)
|
|
.filter { $0.conformances.contains(where: { $0.conformance.string == "XCTestCase" }) }
|
|
|
|
guard !xcTestSuites.isEmpty,
|
|
!xcTestSuites.contains(where: { $0.hasUnsupportedXCTestFunctionality() })
|
|
else { return }
|
|
|
|
// Find extensions of the test case types in the same file
|
|
let xcTestSuiteNames = Set(xcTestSuites.compactMap(\.name))
|
|
let xcTestSuiteExtensions = declarations
|
|
.compactMap(\.asTypeDeclaration)
|
|
.filter { $0.keyword == "extension" && xcTestSuiteNames.contains($0.name ?? "") }
|
|
|
|
// Check if any extension has unsupported functionality
|
|
guard !xcTestSuiteExtensions.contains(where: { $0.hasUnsupportedXCTestFunctionality() })
|
|
else { return }
|
|
|
|
// Replace `import XCTest` with `import Testing`.
|
|
// XCTest also exports Foundation, so add an explicit Foundation import for compatibility.
|
|
formatter.addImports(["Testing", "Foundation"])
|
|
formatter.removeImports(["XCTest"])
|
|
|
|
// XCTest also exports UIKit. To maintain compatibility, add an explicit UIKit import
|
|
// if the test case file references any symbols that appear to come from UIKit.
|
|
if formatter.referencesUIKitSymbols() {
|
|
formatter.addImports(["UIKit"])
|
|
}
|
|
|
|
for xcTestSuite in xcTestSuites {
|
|
xcTestSuite.convertToSwiftTestingSuite()
|
|
}
|
|
|
|
// Also convert test methods in extensions of the test case types
|
|
for xcTestSuiteExtension in xcTestSuiteExtensions {
|
|
xcTestSuiteExtension.convertToSwiftTestingSuite()
|
|
}
|
|
|
|
formatter.forEach(.identifier) { identifierIndex, token in
|
|
if token.string.hasPrefix("XCT") {
|
|
formatter.convertXCTestHelperToSwiftTestingExpectation(at: identifierIndex)
|
|
}
|
|
}
|
|
} examples: {
|
|
"""
|
|
```diff
|
|
@testable import MyFeatureLib
|
|
- import XCTest
|
|
+ import Testing
|
|
+ import Foundation
|
|
|
|
- final class MyFeatureTests: XCTestCase {
|
|
- func testMyFeatureHasNoBugs() {
|
|
- let myFeature = MyFeature()
|
|
- myFeature.runAction()
|
|
- XCTAssertFalse(myFeature.hasBugs, "My feature has no bugs")
|
|
- XCTAssertEqual(myFeature.crashes.count, 0, "My feature doesn't crash")
|
|
- XCTAssertNil(myFeature.crashReport)
|
|
- }
|
|
- }
|
|
+ final class MyFeatureTests {
|
|
+ @Test func myFeatureHasNoBugs() {
|
|
+ let myFeature = MyFeature()
|
|
+ myFeature.runAction()
|
|
+ #expect(!myFeature.hasBugs, "My feature has no bugs")
|
|
+ #expect(myFeature.crashes.isEmpty, "My feature doesn't crash")
|
|
+ #expect(myFeature.crashReport == nil)
|
|
+ }
|
|
+ }
|
|
|
|
- final class MyFeatureTests: XCTestCase {
|
|
- var myFeature: MyFeature!
|
|
-
|
|
- override func setUp() async throws {
|
|
- myFeature = try await MyFeature()
|
|
- }
|
|
-
|
|
- override func tearDown() {
|
|
- myFeature = nil
|
|
- }
|
|
-
|
|
- func testMyFeatureWorks() {
|
|
- myFeature.runAction()
|
|
- XCTAssertTrue(myFeature.worksProperly)
|
|
- XCTAssertEqual(myFeature.screens.count, 8)
|
|
- }
|
|
- }
|
|
+ final class MyFeatureTests {
|
|
+ var myFeature: MyFeature!
|
|
+
|
|
+ init() async throws {
|
|
+ myFeature = try await MyFeature()
|
|
+ }
|
|
+
|
|
+ deinit {
|
|
+ myFeature = nil
|
|
+ }
|
|
+
|
|
+ @Test func myFeatureWorks() {
|
|
+ myFeature.runAction()
|
|
+ #expect(myFeature.worksProperly)
|
|
+ #expect(myFeature.screens.count == 8)
|
|
+ }
|
|
+ }
|
|
```
|
|
"""
|
|
}
|
|
}
|
|
|
|
// MARK: XCTestCase test suite conversion
|
|
|
|
extension TypeDeclaration {
|
|
/// Whether or not this declaration uses XCTest functionality that is
|
|
/// not supported by the preferSwiftTesting rule.
|
|
func hasUnsupportedXCTestFunctionality() -> Bool {
|
|
let overriddenMethods = body.filter {
|
|
$0.modifiers.contains("override")
|
|
}
|
|
|
|
let supportedOverrides = Set(["setUp", "setUpWithError", "tearDown"])
|
|
|
|
for overriddenMethod in overriddenMethods {
|
|
guard let methodName = overriddenMethod.name,
|
|
supportedOverrides.contains(methodName)
|
|
else { return true }
|
|
|
|
// async / throws `tearDown` can't be converted to a `deinit`
|
|
if methodName == "tearDown",
|
|
overriddenMethod.keyword == "func",
|
|
let startOfArguments = formatter.index(of: .startOfScope("("), after: overriddenMethod.keywordIndex),
|
|
let endOfArguments = formatter.endOfScope(at: startOfArguments),
|
|
let effect = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: endOfArguments),
|
|
["async", "throws"].contains(tokens[effect].string)
|
|
{
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/// Converts this XCTestCase implementation to a Swift Testing test suite.
|
|
/// For non-extension types, removes XCTestCase conformance and adds suite attributes.
|
|
/// For extensions, only converts test methods without modifying the type declaration.
|
|
func convertToSwiftTestingSuite() {
|
|
// Only remove conformance and add attributes for non-extension declarations
|
|
let isExtension = keyword == "extension"
|
|
|
|
if !isExtension {
|
|
// Remove the XCTestCase conformance
|
|
if let xcTestCaseConformance = conformances.first(where: { $0.conformance.string == "XCTestCase" }) {
|
|
formatter.removeConformance(at: xcTestCaseConformance.index)
|
|
}
|
|
|
|
// Allow the user to specify additional attributes to add to the new test suite,
|
|
// like `@MainActor`, `@Suite(.serialized)`, etc.
|
|
let attributesToAdd = formatter.options.defaultTestSuiteAttributes.joined(separator: " ")
|
|
if !attributesToAdd.isEmpty {
|
|
let startOfModifiers = formatter.startOfModifiers(at: keywordIndex, includingAttributes: true)
|
|
let attributesWithNewline = attributesToAdd + "\n"
|
|
formatter.insert(tokenize(attributesWithNewline), at: startOfModifiers)
|
|
}
|
|
}
|
|
|
|
let instanceMethods = body.filter { $0.keyword == "func" && !$0.modifiers.contains("static") }
|
|
|
|
for instanceMethod in instanceMethods {
|
|
guard let methodName = instanceMethod.name,
|
|
let startOfParameters = formatter.index(of: .startOfScope("("), after: instanceMethod.keywordIndex),
|
|
let endOfParameters = formatter.endOfScope(at: startOfParameters),
|
|
let startOfFunctionBody = formatter.index(of: .startOfScope("{"), after: endOfParameters),
|
|
let endOfFunctionBody = formatter.endOfScope(at: startOfFunctionBody)
|
|
else { continue }
|
|
|
|
// Convert the setUp method to an initializer
|
|
if methodName == "setUp" || methodName == "setUpWithError" {
|
|
formatter.convertXCTestOverride(
|
|
at: instanceMethod.keywordIndex,
|
|
toLifecycleMethod: "init"
|
|
)
|
|
}
|
|
|
|
// Convert the tearDown method to a deinit
|
|
if methodName == "tearDown" {
|
|
formatter.convertXCTestOverride(
|
|
at: instanceMethod.keywordIndex,
|
|
toLifecycleMethod: "deinit"
|
|
)
|
|
}
|
|
|
|
// Convert any test case method to a @Test method
|
|
if methodName.hasPrefix("test") {
|
|
let arguments = formatter.parseFunctionDeclarationArguments(startOfScope: startOfParameters)
|
|
guard arguments.isEmpty else { continue }
|
|
|
|
// In Swift Testing, idiomatic test case names don't start with "test".
|
|
formatter.removeTestPrefix(fromFunctionAt: instanceMethod.keywordIndex)
|
|
|
|
// XCTest assertions have throwing autoclosures, so can include a `try`
|
|
// without the test case being `throws`. If the test case method isn't `throws`
|
|
// but has any `try`s in the method body, we have to add `throws`.
|
|
if !tokens[endOfParameters ..< startOfFunctionBody].contains(.keyword("throws")),
|
|
tokens[startOfFunctionBody ... endOfFunctionBody].contains(.keyword("try")),
|
|
let indexBeforeStartOfFunctionBody = formatter.index(of: .nonSpaceOrComment, before: startOfFunctionBody)
|
|
{
|
|
formatter.insert([.space(" "), .keyword("throws")], at: indexBeforeStartOfFunctionBody + 1)
|
|
}
|
|
|
|
// Add the @Test macro
|
|
formatter.insert(tokenize("@Test "), at: formatter.startOfModifiers(at: instanceMethod.keywordIndex, includingAttributes: true))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: XCTest function helpers
|
|
|
|
extension Formatter {
|
|
/// Whether or not the file contains an XCTest helper function that
|
|
/// isn't supported by the preferSwiftTesting rule.
|
|
func hasUnsupportedXCTestHelper() -> Bool {
|
|
// https://developer.apple.com/documentation/xctest/xctestcase
|
|
let xcTestCaseInstanceMethods = Set(["expectation", "wait", "measure", "measureMetrics", "addTeardownBlock", "runsForEachTargetApplicationUIConfiguration", "continueAfterFailure", "executionTimeAllowance", "startMeasuring", "stopMeasuring", "defaultPerformanceMetrics", "defaultMetrics", "defaultMeasureOptions", "fulfillment", "addUIInterruptionMonitor", "keyValueObservingExpectation", "removeUIInterruptionMonitor"])
|
|
.union(options.additionalXCTestSymbols)
|
|
|
|
for index in tokens.indices where tokens[index].isIdentifier {
|
|
let identifier = tokens[index].string
|
|
|
|
if xcTestCaseInstanceMethods.contains(identifier) {
|
|
return true
|
|
}
|
|
|
|
// We know how to handle XCTestCase, XCTest, and any XCTAssert variant implemented in `swiftTestingExpectationForXCTestHelper`.
|
|
if tokens[index].string.hasPrefix("XC") {
|
|
let previousToken = lastToken(before: index, where: { !$0.isSpaceOrCommentOrLinebreak })
|
|
switch identifier {
|
|
case "XCTestCase":
|
|
if previousToken != .delimiter(":") {
|
|
return true
|
|
}
|
|
|
|
case "XCTest":
|
|
if previousToken != .keyword("import") {
|
|
return true
|
|
}
|
|
|
|
default:
|
|
if swiftTestingExpectationForXCTestHelper(at: index) == nil {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/// Whether or not this file includes a symbol starting with the UI prefix, which indicates that it probably comes from the UIKit library.
|
|
func referencesUIKitSymbols() -> Bool {
|
|
let allIdentifiersInFile = Set(tokens.lazy.filter(\.isIdentifier).map(\.string))
|
|
return allIdentifiersInFile.contains(where: { $0.hasPrefix("UI") })
|
|
}
|
|
|
|
/// Converts the XCTest helper function (e.g. `XCTAssert(...)`) at the given index
|
|
/// to a Swift Testng expectation (e.g. `#expect(...)`).
|
|
func convertXCTestHelperToSwiftTestingExpectation(at identifierIndex: Int) {
|
|
guard let swiftTestingExpectation = swiftTestingExpectationForXCTestHelper(at: identifierIndex),
|
|
let startOfFunctionCall = index(of: .startOfScope("("), after: identifierIndex),
|
|
let endOfFunctionCall = endOfScope(at: startOfFunctionCall)
|
|
else { return }
|
|
|
|
replaceTokens(in: identifierIndex ... endOfFunctionCall, with: swiftTestingExpectation)
|
|
}
|
|
|
|
/// Computes the Swift Testing expectation (e.g. `#expect(...)`)
|
|
/// for the XCTest helper function (e.g. `XCTAssert(...)`) at the given index.
|
|
/// Returns `nil` if this XCTest helper function is unsupported.
|
|
func swiftTestingExpectationForXCTestHelper(at identifierIndex: Int) -> [Token]? {
|
|
guard tokens[identifierIndex].isIdentifier,
|
|
tokens[identifierIndex].string.hasPrefix("XCT"),
|
|
let startOfFunctionCall = index(of: .nonSpaceOrComment, after: identifierIndex)
|
|
else { return nil }
|
|
|
|
switch tokens[identifierIndex].string {
|
|
case "XCTAssert":
|
|
return convertXCTAssertToTestingExpectation(at: identifierIndex) { value in
|
|
value
|
|
}
|
|
|
|
case "XCTAssertTrue":
|
|
return convertXCTAssertToTestingExpectation(at: identifierIndex) { value in
|
|
value
|
|
}
|
|
|
|
case "XCTAssertFalse":
|
|
return convertXCTAssertToTestingExpectation(at: identifierIndex) { value in
|
|
// Unlike other operators which are whitespace insensitive, the ! token has to come immediately before the first
|
|
// non-space/non-comment token in the rhs value, and after any effect like `try await`.
|
|
// ! also has stronger associativity than other operators, (for example, `!foo == bar` would be incorrect),
|
|
// so we have to wrap the value in parens if it includes any infix operators.
|
|
var tokens = tokenize(value.wrappedInParensIfContainsOperatorOrTry())
|
|
if let firstTokenIndex = tokens.firstIndex(where: { !$0.isSpaceOrCommentOrLinebreak }) {
|
|
tokens.insert(.operator("!", .prefix), at: firstTokenIndex)
|
|
}
|
|
|
|
return tokens.string
|
|
}
|
|
|
|
case "XCTAssertNil":
|
|
return convertXCTAssertToTestingExpectation(at: identifierIndex) { value in
|
|
"\(value.wrappedInParensIfContainsOperatorOrTry()) == nil"
|
|
}
|
|
|
|
case "XCTAssertNotNil":
|
|
return convertXCTAssertToTestingExpectation(at: identifierIndex) { value in
|
|
"\(value.wrappedInParensIfContainsOperatorOrTry()) != nil"
|
|
}
|
|
|
|
case "XCTAssertEqual":
|
|
return convertXCTComparisonToTestingExpectation(
|
|
at: identifierIndex,
|
|
operator: "=="
|
|
)
|
|
|
|
case "XCTAssertNotEqual":
|
|
return convertXCTComparisonToTestingExpectation(
|
|
at: identifierIndex,
|
|
operator: "!="
|
|
)
|
|
|
|
case "XCTAssertIdentical":
|
|
return convertXCTComparisonToTestingExpectation(
|
|
at: identifierIndex,
|
|
operator: "==="
|
|
)
|
|
|
|
case "XCTAssertNotIdentical":
|
|
return convertXCTComparisonToTestingExpectation(
|
|
at: identifierIndex,
|
|
operator: "!=="
|
|
)
|
|
|
|
case "XCTAssertGreaterThan":
|
|
return convertXCTComparisonToTestingExpectation(
|
|
at: identifierIndex,
|
|
operator: ">"
|
|
)
|
|
|
|
case "XCTAssertGreaterThanOrEqual":
|
|
return convertXCTComparisonToTestingExpectation(
|
|
at: identifierIndex,
|
|
operator: ">="
|
|
)
|
|
|
|
case "XCTAssertLessThan":
|
|
return convertXCTComparisonToTestingExpectation(
|
|
at: identifierIndex,
|
|
operator: "<"
|
|
)
|
|
|
|
case "XCTAssertLessThanOrEqual":
|
|
return convertXCTComparisonToTestingExpectation(
|
|
at: identifierIndex,
|
|
operator: "<="
|
|
)
|
|
|
|
case "XCTFail":
|
|
let functionParams = parseFunctionCallArguments(startOfScope: startOfFunctionCall, preserveWhitespace: true)
|
|
switch functionParams.count {
|
|
case 0:
|
|
return tokenize("Issue.record()")
|
|
case 1:
|
|
return tokenize("Issue.record(\(functionParams[0].value.asSwiftTestingComment()))")
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
case "XCTUnwrap":
|
|
let functionParams = parseFunctionCallArguments(startOfScope: startOfFunctionCall, preserveWhitespace: true)
|
|
switch functionParams.count {
|
|
case 1:
|
|
return tokenize("#require(\(functionParams[0].value))")
|
|
case 2:
|
|
return tokenize("#require(\(functionParams[0].value),\(functionParams[1].value.asSwiftTestingComment()))")
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
case "XCTAssertNoThrow":
|
|
let functionParams = parseFunctionCallArguments(startOfScope: startOfFunctionCall, preserveWhitespace: true)
|
|
switch functionParams.count {
|
|
case 1:
|
|
return tokenize("#expect(throws: Never.self) { \(functionParams[0].value) }")
|
|
case 2:
|
|
return tokenize("#expect(throws: Never.self,\(functionParams[1].value.asSwiftTestingComment())) { \(functionParams[0].value) }")
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
case "XCTAssertThrowsError":
|
|
let functionParams = parseFunctionCallArguments(startOfScope: startOfFunctionCall, preserveWhitespace: true)
|
|
|
|
// Trailing closure variant is unsupported for now
|
|
if let endOfFunctionCall = endOfScope(at: startOfFunctionCall),
|
|
let startOfTrailingClosure = index(of: .nonSpaceOrCommentOrLinebreak, after: endOfFunctionCall),
|
|
tokens[startOfTrailingClosure] == .startOfScope("{")
|
|
{ return nil }
|
|
|
|
switch functionParams.count {
|
|
case 1:
|
|
return tokenize("#expect(throws: Error.self) { \(functionParams[0].value) }")
|
|
case 2:
|
|
return tokenize("#expect(throws: Error.self,\(functionParams[1].value.asSwiftTestingComment())) { \(functionParams[0].value) }")
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Converts a single-value XCTest assertion like XCTAssertTrue or XCTAssertNil
|
|
/// to a Swift Testing expectation. Supports an optional message.
|
|
func convertXCTAssertToTestingExpectation(
|
|
at identifierIndex: Int,
|
|
makeAssertion: (_ value: String) -> String
|
|
) -> [Token]? {
|
|
guard let startOfFunctionCall = index(of: .nonSpaceOrComment, after: identifierIndex) else { return nil }
|
|
let functionParams = parseFunctionCallArguments(startOfScope: startOfFunctionCall, preserveWhitespace: true)
|
|
|
|
// All of the function params should be unlabeled
|
|
guard functionParams.allSatisfy({ $0.label == nil }) else { return nil }
|
|
|
|
let value: String
|
|
let message: String?
|
|
switch functionParams.count {
|
|
case 1:
|
|
value = functionParams[0].value
|
|
message = nil
|
|
case 2:
|
|
value = functionParams[0].value
|
|
message = functionParams[1].value.asSwiftTestingComment()
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
if let message {
|
|
return tokenize("#expect(\(makeAssertion(value)),\(message))")
|
|
} else {
|
|
return tokenize("#expect(\(makeAssertion(value)))")
|
|
}
|
|
}
|
|
|
|
/// Converts a single-value XCTest assertion like XCTAssertTrue or XCTAssertNil
|
|
/// to a Swift Testing expectation. Supports an optional message.
|
|
func convertXCTComparisonToTestingExpectation(
|
|
at identifierIndex: Int,
|
|
operator operatorToken: String
|
|
) -> [Token]? {
|
|
guard let startOfFunctionCall = index(of: .nonSpaceOrComment, after: identifierIndex) else { return nil }
|
|
let functionParams = parseFunctionCallArguments(startOfScope: startOfFunctionCall, preserveWhitespace: true)
|
|
|
|
// All of the function params should be unlabeled
|
|
guard functionParams.allSatisfy({ $0.label == nil }) else { return nil }
|
|
|
|
let lhs: String
|
|
let rhs: String
|
|
let message: String?
|
|
switch functionParams.count {
|
|
case 2:
|
|
lhs = functionParams[0].value.wrappedInParensIfContainsOperatorOrTry()
|
|
rhs = functionParams[1].value.wrappedInParensIfContainsOperatorOrTry()
|
|
message = nil
|
|
case 3:
|
|
lhs = functionParams[0].value.wrappedInParensIfContainsOperatorOrTry()
|
|
rhs = functionParams[1].value.wrappedInParensIfContainsOperatorOrTry()
|
|
message = functionParams[2].value.asSwiftTestingComment()
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
if let message {
|
|
return tokenize("#expect(\(lhs) \(operatorToken)\(rhs),\(message))")
|
|
} else {
|
|
return tokenize("#expect(\(lhs) \(operatorToken)\(rhs))")
|
|
}
|
|
}
|
|
|
|
/// Converts the XCTest override method `setUp` or `tearDown` to the given lifecycle method
|
|
func convertXCTestOverride(at keywordIndex: Int, toLifecycleMethod lifecycleMethodName: String) {
|
|
guard let nameIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: keywordIndex),
|
|
let startOfArgumentsIndex = index(of: .startOfScope("("), after: nameIndex),
|
|
let endOfArgumentsIndex = endOfScope(at: startOfArgumentsIndex),
|
|
let startOfFunctionBody = index(of: .startOfScope("{"), after: endOfArgumentsIndex),
|
|
let endOfFunctionBody = endOfScope(at: startOfFunctionBody)
|
|
else { return }
|
|
|
|
// Remove `super.setUp()` / `super.tearDown()` if present
|
|
if let superCall = index(of: .identifier("super"), in: startOfFunctionBody + 1 ..< endOfFunctionBody),
|
|
let dotIndex = index(of: .nonSpaceOrLinebreak, after: superCall),
|
|
tokens[dotIndex] == .operator(".", .infix),
|
|
let methodName = index(of: .nonSpaceOrCommentOrLinebreak, after: dotIndex),
|
|
tokens[methodName] == tokens[nameIndex],
|
|
let startOfCall = index(of: .nonSpaceOrCommentOrLinebreak, after: methodName),
|
|
tokens[startOfCall] == .startOfScope("("),
|
|
let endOfCall = endOfScope(at: startOfCall)
|
|
{
|
|
removeTokens(in: startOfLine(at: superCall) ... endOfCall + 1)
|
|
}
|
|
|
|
// Replace `func setUp` with `init`, or `func tearDown` with `deinit`.
|
|
// For `deinit`, we also have to remove the parens from the `tearDown()` method.
|
|
if lifecycleMethodName == "deinit" {
|
|
replaceTokens(in: keywordIndex ... endOfArgumentsIndex, with: [.keyword(lifecycleMethodName)])
|
|
} else {
|
|
replaceTokens(in: keywordIndex ... nameIndex, with: [.keyword(lifecycleMethodName)])
|
|
}
|
|
|
|
// Remove the `override` modifier
|
|
if let overrideModifier = indexOfModifier("override", forDeclarationAt: keywordIndex) {
|
|
removeTokens(in: overrideModifier ... overrideModifier + 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension String {
|
|
/// Converts this value to a comment that can be used as a Swift Testing `Comment` value,
|
|
/// which is `ExpressibleByStringLiteral` but not a `String` itself.
|
|
func asSwiftTestingComment() -> String {
|
|
let formatter = Formatter(tokenize(self))
|
|
|
|
// If the entire value is a string literal, we can use it directly as
|
|
// a Swift Testing comment literal.
|
|
if let startOfString = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: -1),
|
|
formatter.tokens[startOfString].isStringDelimiter,
|
|
let endOfString = formatter.endOfScope(at: startOfString),
|
|
formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: endOfString) == nil
|
|
{
|
|
return self
|
|
}
|
|
|
|
else {
|
|
let leadingSpaces = formatter.currentIndentForLine(at: 0)
|
|
return leadingSpaces + "Comment(rawValue: \(trimmingCharacters(in: .whitespaces)))"
|
|
}
|
|
}
|
|
|
|
/// Wraps this value in parens if the value contains an infix operator or leading try keyword.
|
|
/// For example, `!foo == bar` and `!(foo == bar)` have different meanings,
|
|
/// and `try? foo() == bar` and (`(try? foo()) == bar` have different meanings.
|
|
func wrappedInParensIfContainsOperatorOrTry() -> String {
|
|
let formatter = Formatter(tokenize(self))
|
|
|
|
guard let firstTokenIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: -1),
|
|
let lastTokenIndex = formatter.lastIndex(of: .nonSpaceOrCommentOrLinebreak, in: formatter.tokens.indices)
|
|
else { return formatter.tokens.string }
|
|
|
|
// If the operator if nested in parens or a closure, then we don't need extra parens.
|
|
// If we find a startOfScope, skip the the end of that scope.
|
|
var index = firstTokenIndex
|
|
var hasInfixOperatorOrTry = false
|
|
|
|
if formatter.tokens[firstTokenIndex] == .keyword("try") {
|
|
hasInfixOperatorOrTry = true
|
|
}
|
|
|
|
while index < formatter.tokens.indices.last! {
|
|
let token = formatter.tokens[index]
|
|
|
|
if token.isStartOfScope, let endOfScope = formatter.endOfScope(at: index) {
|
|
index = endOfScope
|
|
continue
|
|
}
|
|
|
|
if (token.isOperator(ofType: .infix) && !token.isOperator("."))
|
|
|| token == .keyword("is") // the is keyword acts like an infix operator
|
|
{
|
|
hasInfixOperatorOrTry = true
|
|
break
|
|
}
|
|
|
|
index += 1
|
|
}
|
|
|
|
if hasInfixOperatorOrTry {
|
|
formatter.insert(.endOfScope(")"), at: lastTokenIndex + 1)
|
|
formatter.insert(.startOfScope("("), at: firstTokenIndex)
|
|
}
|
|
|
|
return formatter.tokens.string
|
|
}
|
|
}
|