diff --git a/Sources/SwiftRefactor/CMakeLists.txt b/Sources/SwiftRefactor/CMakeLists.txt index 8a7325f37ed..46df20e1e24 100644 --- a/Sources/SwiftRefactor/CMakeLists.txt +++ b/Sources/SwiftRefactor/CMakeLists.txt @@ -18,6 +18,7 @@ add_swift_syntax_library(SwiftRefactor ExpandEditorPlaceholder.swift FormatRawStringLiteral.swift IntegerLiteralUtilities.swift + InvertIfCondition.swift MigrateToNewIfLetSyntax.swift OpaqueParameterToGeneric.swift RefactoringProvider.swift diff --git a/Sources/SwiftRefactor/InvertIfCondition.swift b/Sources/SwiftRefactor/InvertIfCondition.swift new file mode 100644 index 00000000000..8b246a0b531 --- /dev/null +++ b/Sources/SwiftRefactor/InvertIfCondition.swift @@ -0,0 +1,82 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if compiler(>=6) +public import SwiftSyntax +#else +import SwiftSyntax +#endif + +/// Inverts a negated `if` condition and swaps the branches. +/// +/// ## Before +/// +/// ```swift +/// if !x { +/// foo() +/// } else { +/// bar() +/// } +/// ``` +/// +/// ## After +/// +/// ```swift +/// if x { +/// bar() +/// } else { +/// foo() +/// } +/// ``` +public struct InvertIfCondition: SyntaxRefactoringProvider { + public static func refactor(syntax ifExpr: IfExprSyntax, in context: Void) -> IfExprSyntax { + guard let elseBody = ifExpr.elseBody, case .codeBlock(let elseBlock) = elseBody else { + return ifExpr + } + + guard ifExpr.conditions.count == 1, let condition = ifExpr.conditions.first else { + return ifExpr + } + + guard case .expression(let expr) = condition.condition else { + return ifExpr + } + + guard let prefixOpExpr = expr.as(PrefixOperatorExprSyntax.self), prefixOpExpr.operator.text == "!" else { + return ifExpr + } + + let innerExpr = prefixOpExpr.expression.with(\.leadingTrivia, prefixOpExpr.leadingTrivia.merging(triviaOf: prefixOpExpr.operator)) + + let newCondition = condition.with(\.condition, .expression(innerExpr)) + let newConditions = ifExpr.conditions.with(\.[ifExpr.conditions.startIndex], newCondition) + + let oldBody = ifExpr.body + let oldElseBlock = elseBlock + + let newBody = + oldElseBlock + .with(\.leadingTrivia, oldBody.leadingTrivia) + .with(\.trailingTrivia, oldBody.trailingTrivia) + + let newElseBody = + oldBody + .with(\.leadingTrivia, oldElseBlock.leadingTrivia) + .with(\.trailingTrivia, oldElseBlock.trailingTrivia) + + return + ifExpr + .with(\.conditions, newConditions) + .with(\.body, newBody) + .with(\.elseBody, .codeBlock(newElseBody)) + } +} diff --git a/Tests/SwiftRefactorTest/InvertIfConditionTest.swift b/Tests/SwiftRefactorTest/InvertIfConditionTest.swift new file mode 100644 index 00000000000..5549fcdb8af --- /dev/null +++ b/Tests/SwiftRefactorTest/InvertIfConditionTest.swift @@ -0,0 +1,162 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftParser +import SwiftRefactor +import SwiftSyntax +import SwiftSyntaxBuilder +import XCTest +import _SwiftSyntaxTestSupport + +final class InvertIfConditionTest: XCTestCase { + func testInvertIfCondition() throws { + let tests = [ + ( + """ + if !x { + foo() + } else { + bar() + } + """, + """ + if x { + bar() + } else { + foo() + } + """ + ), + ( + """ + if !(x == y) { + return + } else { + continue + } + """, + """ + if (x == y) { + continue + } else { + return + } + """ + ), + // Trivia preservation + ( + """ + if /* comment */ !x { + a + } else { + b + } + """, + """ + if /* comment */ x { + b + } else { + a + } + """ + ), + ] + + for (input, expected) in tests { + try assertInvertIfCondition(input, expected: expected) + } + } + + func testInvertIfConditionFails() throws { + let tests = [ + // Not negated + """ + if x { + a + } else { + b + } + """, + // No else + """ + if !x { + a + } + """, + // Else if (not a CodeBlock) + """ + if !x { + a + } else if y { + b + } + """, + // Multiple conditions + """ + if !x, !y { + a + } else { + b + } + """, + // Binding + """ + if let x = y { + a + } else { + b + } + """, + ] + + for input in tests { + try assertInvertIfCondition(input, expected: input) + } + } + + private func assertInvertIfCondition( + _ input: String, + expected: String, + file: StaticString = #filePath, + line: UInt = #line + ) throws { + let inputSyntax = try XCTUnwrap( + ExprSyntax.parse(from: input).as(IfExprSyntax.self), + "Failed validity check: \(input)", + file: file, + line: line + ) + let expectedSyntax = try XCTUnwrap( + ExprSyntax.parse(from: expected), + "Failed validity check: \(expected)", + file: file, + line: line + ) + + try assertRefactor( + inputSyntax, + context: (), + provider: InvertIfCondition.self, + expected: expectedSyntax, + file: file, + line: line + ) + } +} + +// Private helper to avoid redeclaration conflicts +private extension ExprSyntax { + static func parse(from source: String) -> ExprSyntax { + var parser = Parser(source) + return ExprSyntax.parse(from: &parser) + } +}