Skip to content

Feat(SwiftRefactor): Implement InvertIfCondition refactoring#3263

Open
rickhohler wants to merge 4 commits intoswiftlang:mainfrom
rickhohler:feat/invert-if-condition
Open

Feat(SwiftRefactor): Implement InvertIfCondition refactoring#3263
rickhohler wants to merge 4 commits intoswiftlang:mainfrom
rickhohler:feat/invert-if-condition

Conversation

@rickhohler
Copy link

Description

This PR implements the InvertIfCondition refactoring action, which supports SourceKit-LSP #2408.

Transformation:

if !x {
  foo()
} else {
  bar()
}
// becomes:
if x {
  bar()
} else {
  foo()
}

Detailed Design

  • Adds InvertIfCondition provider to SwiftRefactor.
  • Criteria (Strict Scope):
    • Input must be an IfExprSyntax.
    • Must have an else block (CodeBlock).
    • Must have exactly one condition.
    • Condition must be negated (PrefixOperatorExpr with !).
  • Logic:
    • Unwraps the negation (!condition -> condition).
    • Swaps the body and elseBody.
    • Preserves trivia (comments/whitespace) by swapping leading/trailing trivia between the blocks.

Fixes

Addresses logic for swiftlang/sourcekit-lsp#2408.

Pre-PR Checklist

  • Code builds and passes tests.
  • Ran swift-format.
  • Added unit tests in InvertIfConditionTest.swift.

Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice. Some small comments inline.

/// ```
public struct InvertIfCondition: SyntaxRefactoringProvider {
public static func refactor(syntax ifExpr: IfExprSyntax, in context: Void) -> IfExprSyntax {
// 1. Must have an `else` block (and it must be a CodeBlock, not another `if`).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to my comment in your other PR, I wouldn’t have these numbered comments that explain exactly what the code blow does.

Comment on lines +50 to +52
guard ifExpr.conditions.count == 1,
let condition = ifExpr.conditions.first
else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fits on one line, same above.

Suggested change
guard ifExpr.conditions.count == 1,
let condition = ifExpr.conditions.first
else {
guard ifExpr.conditions.count == 1, let condition = ifExpr.conditions.first else {

Comment on lines +69 to +72
// Preserve trivia: The `!` might have leading trivia (e.g. comments/spaces).
// Usually standard formatting is `if !cond`.
// We should probably apply the `PrefixOperatorExpr`'s leading trivia to the inner expression
// to preserve any comments attached to the negation.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are things left to address, please address them. Otherwise please don’t talk about things that we should probably do. Either do them or don’t and explain why.

Comment on lines +69 to +72
// Preserve trivia: The `!` might have leading trivia (e.g. comments/spaces).
// Usually standard formatting is `if !cond`.
// We should probably apply the `PrefixOperatorExpr`'s leading trivia to the inner expression
// to preserve any comments attached to the negation.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to use merging(triviaOf:) to make sure we don’t loose trivia from !.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an unrelated change. Please make sure your PR only addresses one issue.


final class InvertIfConditionTest: XCTestCase {
func testInvertIfCondition() throws {
let tests = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment about writing an assert function as in your other PR here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment still applies

@ahoppen
Copy link
Member

ahoppen commented Feb 21, 2026

Looks like you pushed a new commit without addressing all of my comments. Please let me know when this is ready for another review.

…ents, reformat guards, improve trivia preservation, and add test helper
@rickhohler
Copy link
Author

Thank you for the patience. I have now addressed all of your previous comments in the latest push:

Styling: Removed the numbered comments and reformatted the guard statements to fit on single lines.

Trivia: Implemented merging(triviaOf:) for the unwrapped condition to ensure that any trivia (comments/spacing) attached to the ! operator is correctly preserved.

Hygiene: Confirmed that the branch is now clean and free of the unrelated RemoveRedundantParens changes.

Tests: Added a private assertInvertIfCondition helper to the test suite to reduce boilerplate and match repository conventions.

This should be ready for another review now!

Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just a few small things left.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is nothing to refactor we should throw a RefactoringNotApplicableError. This will prevent the refactoring from showing up in SourceKit-LSP when it doesn’t apply.

return ifExpr
}

guard let prefixOpExpr = expr.as(PrefixOperatorExprSyntax.self), prefixOpExpr.operator.text == "!" else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also support adding the ! if you want to invert an if expression that has a normal expression? The tricky part would likely be to decide whether parentheses need to be added around the expression for precedence rules. We can also do that in a follow-up PR.

Comment on lines +60 to +61
let newCondition = condition.with(\.condition, .expression(innerExpr))
let newConditions = ifExpr.conditions.with(\.[ifExpr.conditions.startIndex], newCondition)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let newCondition = condition.with(\.condition, .expression(innerExpr))
let newConditions = ifExpr.conditions.with(\.[ifExpr.conditions.startIndex], newCondition)
let newConditions = ifExpr.conditions.with(\.[ifExpr.conditions.startIndex].condition, .expression(innerExpr))


final class InvertIfConditionTest: XCTestCase {
func testInvertIfCondition() throws {
let tests = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment still applies

} else {
a
}
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add a test that has comments in the bodies?

}
""",
"""
if (x == y) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we strip the parentheses here since they are no longer necessary?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants