Skip to content

Comments

Feat(SwiftRefactor): Implement CollapseNestedIfStmt refactoring#3260

Open
rickhohler wants to merge 2 commits intoswiftlang:mainfrom
rickhohler:feat/collapse-nested-if
Open

Feat(SwiftRefactor): Implement CollapseNestedIfStmt refactoring#3260
rickhohler wants to merge 2 commits intoswiftlang:mainfrom
rickhohler:feat/collapse-nested-if

Conversation

@rickhohler
Copy link

Description

This PR implements the CollapseNestedIfStmt refactoring action, which merges nested if statements into a single if statement with a combined condition list.

Transformation:

if a {
  if b {
    print("hello")
  }
}
// becomes:
if a, b {
  print("hello")
}

Detailed Design

  • Adds CollapseNestedIfStmt provider to SwiftRefactor.
  • Validation Logic:
    • Ensures outer if has exactly one statement (the inner if).
    • Ensures neither if has an else block (to avoid ambiguity).
    • Checks for IfExprSyntax inside the block.
  • Transformation Logic:
    • Appends inner conditions to outer conditions.
    • Carefully handles comma insertion and whitespace trimming for the merged condition list.

Fixes

Addresses one part of swiftlang/sourcekit-lsp#2424 (Porting legacy refactoring actions).

Pre-PR Checklist

  • Code builds and passes tests.
  • Ran swift-format.
  • Added unit tests in CollapseNestedIfStmtTest.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.

Thanks for the PR, @rickhohler. I think the biggest remaining item to do is to adjust indentation after merging the if statements.

Comment on lines +47 to +49
guard outerIf.body.statements.count == 1,
let firstStatement = outerIf.body.statements.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.

Suggested change
guard outerIf.body.statements.count == 1,
let firstStatement = outerIf.body.statements.first
else {
guard let onlyStatement = outerIf.body.statements.only else {

Comment on lines +53 to +60
// 3. Check if that statement is an `IfExprSyntax` (inner `if`).
// It could be an `expression` statement containing the `IfExpr` or just the expression itself depending on parsing context,
// but typically in a code block it's an expression statement.
guard let expressionStmt = firstStatement.item.as(ExpressionStmtSyntax.self),
let innerIf = expressionStmt.expression.as(IfExprSyntax.self)
else {
return outerIf
}
Copy link
Member

Choose a reason for hiding this comment

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

The code doesn’t account for the fact that firstStatement might be an IfExprSyntax directly, right? So it mismatches the comment.

return outerIf
}

// 2. Ensure the outer `if` body has exactly one statement.
Copy link
Member

Choose a reason for hiding this comment

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

I personally wouldn’t add numbered lists here because it makes it harder to insert more checks in the future, if necessary. At which point this comment becomes trivial because it describes exactly what’s done below and can thus be removed.

Comment on lines +72 to +74
// If the last condition doesn't have a trailing comma, verify we need to add one.
// Actually, `ConditionElementListSyntax` elements usually handle commas.
// We just need to make sure the last element of the *outer* list has a comma now that it's not last.
Copy link
Member

Choose a reason for hiding this comment

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

You seem to contradict the first line of this comment in the second line. Could you merge these comments to make them consistent? Probably also the one right after 5. Merge conditions.

Comment on lines +77 to +78
// Remove existing trailing spaces/trivia before comma to be clean, logic can be refined.
// But crucially, we must add a comma.
Copy link
Member

Choose a reason for hiding this comment

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

If the logic can be refined, please do so.

}

// Add inner conditions.
// We might need to adjust trivia on the first inner condition to look nice (e.g., remove leading newlines if any).
Copy link
Member

Choose a reason for hiding this comment

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

You don’t seem to be doing what you describe in the comment though.


final class CollapseNestedIfStmtTest: XCTestCase {
func testCollapseNestedIfStmt() 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.

If any of these tests fail, it’s hard to know which one it was. Could you either use the #line trick we use elsewhere or write an assertCollapseIfStmt function that you call for different inputs. I’d prefer the latter option.

(
"""
if a {
// comment
Copy link
Member

Choose a reason for hiding this comment

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

It would be great if we could retain the comments as much as possible.

Comment on lines +33 to +34
print("hello")
}
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 adjust the indentation here so we get

if a, b {
  print("hello")
}

You can take a look at what we do with IndentationRemover in SourceKit-LSP.

}

extension ExprSyntax {
static func parse(from source: String) -> ExprSyntax {
Copy link
Member

Choose a reason for hiding this comment

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

ExprSyntax conforms to ExpressibleByStringLiteral, so this isn’t necessary if you convert the string literals straight to an ExprSyntax.

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