Skip to content

Conversation

@PhantomInTheWire
Copy link
Contributor

fixes: #1569

mostly works but not sure what to do with many edge cases and has a todo for switch statements, also this will probably have conflicts with #2406 marking as draft till that merges and i can resolve the conflicts.

Screen.Recording.2026-01-04.at.17.03.17.mov

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 a lot for implementing this refactoring, @PhantomInTheWire ❤️

The PR is already pretty large, so I’d suggest we focus on the if -> guard refactoring in this PR first and leave guard -> if for a follow-up PR, to simplify review.

I left some first initial comments in the PR. The overall direction seems great and you thought about many edge cases. I’ll do another thorough review once the first set of comments are addressed.

let lineTable = LineTable(cleanInput)
let sortedEdits = changes.sorted {
(a: LanguageServerProtocol.TextEdit, b: LanguageServerProtocol.TextEdit) -> Bool in
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
(a: LanguageServerProtocol.TextEdit, b: LanguageServerProtocol.TextEdit) -> Bool in
(a: TextEdit, b: TextEdit) -> Bool in

Comment on lines +1117 to +1386
if a.range.lowerBound.line != b.range.lowerBound.line {
return a.range.lowerBound.line > b.range.lowerBound.line
}
return a.range.lowerBound.utf16index > b.range.lowerBound.utf16index
Copy link
Member

Choose a reason for hiding this comment

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

Position should conform to Comparable, so you should be able to compare that straight away.

return a.range.lowerBound.utf16index > b.range.lowerBound.utf16index
}
for edit in sortedEdits {
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 apply(edits:to:) from RenameAssertions, which already does this.

""",
expectedOutput: """
func test() -> Int? {
guard let value = optional else {
Copy link
Member

Choose a reason for hiding this comment

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

Looks like the double whitespace before else shouldn’t be there.

}
/// Tests guard-to-if conversion eligibility directly without LSP overhead.
private func assertConvertibleToIfLet(
Copy link
Member

Choose a reason for hiding this comment

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

Instead of having a separate test function to test whether something is convertible, should we just use validateCodeAction and check that we don’t get a specific code action?

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.

Started reviewing this again but then realized that you haven’t addressed all my previous comments yet (no problem with that, just didn’t realize before I started reviewing), so stopped half-way.

Comment on lines +50 to +53
if parent.is(ExpressionStmtSyntax.self) {
current = parent
continue
}
Copy link
Member

Choose a reason for hiding this comment

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

I think we should only check if the parent of ifExpr is an ExpressionStmtSyntax instead of having a loop. Otherwise we would pick the wrong if statement to convert in the following when invoking on b.

if let a {
  let x = if let b { b } else { nil }
}

let guardStmt = buildGuardStatement(from: ifExpr, elseBody: Array(followingStatements))
let newBodyStatements = ifExpr.body.statements

let lastStatement = followingStatements[followingStatements.index(before: followingStatements.endIndex)]
Copy link
Member

Choose a reason for hiding this comment

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

Isn’t this the same as followingStatements.last? We could avoid the force unwrap of last if we captured the last statement instead of checking for followingStatements.isEmpty.

Comment on lines +83 to +85
let newTrivia: Trivia = .newline + baseIndentation
let unindentedStmt = stmt.with(\.leadingTrivia, newTrivia)
replacementText += unindentedStmt.description
Copy link
Member

Choose a reason for hiding this comment

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

I think this has two issues:

  1. It removes non-whitespace trivia, eg.
if let a {
  // a comment
  return
}
print(a)
  1. It doesn’t properly un-indent multi-line code items, eg.
if let a {
  print(
    "oops"
  )
  return
}
print(a)

This is what I meant in #2420 (comment)

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.

Code action to convert between early-exit if and guard

2 participants