Skip to content

Add ConvertToDoCatch refactoring to SwiftRefactor#3233

Open
Clemo97 wants to merge 5 commits intoswiftlang:mainfrom
Clemo97:main
Open

Add ConvertToDoCatch refactoring to SwiftRefactor#3233
Clemo97 wants to merge 5 commits intoswiftlang:mainfrom
Clemo97:main

Conversation

@Clemo97
Copy link
Copy Markdown

@Clemo97 Clemo97 commented Jan 12, 2026

Summary
This PR implements the ConvertToDoCatch refactoring in swift-syntax, which converts try! (force-try) expressions into proper do-catch blocks with error handling.

Related to swiftlang/sourcekit-lsp#2424

And replaces the sourcekitd implementation https://github.com/swiftlang/swift/blob/9b452820367ccc7b5d9effbc1565bcd945c81768/lib/Refactoring/ConvertToDoCatch.cpp

Copy link
Copy Markdown
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. I have a few comments on the test case but overall this looks good. Thanks for implementing it.

leftBrace: .leftBraceToken(trailingTrivia: .newline),
statements: CodeBlockItemListSyntax([
CodeBlockItemSyntax(
leadingTrivia: .spaces(2),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could we pass in the indentation of the source file through the context, similar to what we do in ExpandEditorPlaceholder?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Actually, sorry for my change of mind, but let’s infer the indentation using BasicFormat.inferIndentation. I just realized that that’s what we’re doing in the majority of other refactoring actions.

let tryExpression = syntax.with(\.questionOrExclamationMark, nil)

// Create the do-catch statement with proper spacing
let doStatement = DoStmtSyntax(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would it be easier to construct this statement using string interpolation? Ie.

let doStatement = DoStmtSyntax("""
do {

}
""")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is still outstanding.

try assertRefactorConvert(baseline, expected: expected)
}

func testForceTryWithVariableBinding() throws {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This test doesn’t contain a variable binding, right?

Comment on lines +55 to +119
func testForceTryWithArguments() throws {
let baseline: ExprSyntax = """
try! processFile(at: path, with: options)
"""

let expected: CodeBlockItemListSyntax = """
do {
try processFile(at: path, with: options)
} catch {
<#code#>
}
"""

try assertRefactorConvert(baseline, expected: expected)
}

func testForceTryWithChaining() throws {
let baseline: ExprSyntax = """
try! getData().process().save()
"""

let expected: CodeBlockItemListSyntax = """
do {
try getData().process().save()
} catch {
<#code#>
}
"""

try assertRefactorConvert(baseline, expected: expected)
}

func testForceTryWithPropertyAccess() throws {
let baseline: ExprSyntax = """
try! object.riskyProperty.value
"""

let expected: CodeBlockItemListSyntax = """
do {
try object.riskyProperty.value
} catch {
<#code#>
}
"""

try assertRefactorConvert(baseline, expected: expected)
}

// MARK: - Complex Expressions

func testForceTryWithSubscript() throws {
let baseline: ExprSyntax = """
try! array[index]
"""

let expected: CodeBlockItemListSyntax = """
do {
try array[index]
} catch {
<#code#>
}
"""

try assertRefactorConvert(baseline, expected: expected)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since we just copy the expression verbatim, I don’t think that these tests provide much value. It’s the exact same code path that’s taken in the refactor action, so I’d remove them

try assertRefactorConvert(baseline, expected: expected)
}

func testForceTryWithMultilineExpression() throws {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should already be covered by testForceTryWithClosure, which also contains a multi-line expression (which is what I think is relevant here)

Comment on lines +228 to +274
func testJSONDecodingExample() throws {
let baseline: ExprSyntax = """
try! JSONDecoder().decode(User.self, from: data)
"""

let expected: CodeBlockItemListSyntax = """
do {
try JSONDecoder().decode(User.self, from: data)
} catch {
<#code#>
}
"""

try assertRefactorConvert(baseline, expected: expected)
}

func testFileReadingExample() throws {
let baseline: ExprSyntax = """
try! String(contentsOf: url, encoding: .utf8)
"""

let expected: CodeBlockItemListSyntax = """
do {
try String(contentsOf: url, encoding: .utf8)
} catch {
<#code#>
}
"""

try assertRefactorConvert(baseline, expected: expected)
}

func testDataWritingExample() throws {
let baseline: ExprSyntax = """
try! data.write(to: fileURL)
"""

let expected: CodeBlockItemListSyntax = """
do {
try data.write(to: fileURL)
} catch {
<#code#>
}
"""

try assertRefactorConvert(baseline, expected: expected)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think all of these are effectively covered by the tests you already have above, so I’d remove them. JSONDecoder is really no different than any other types to swift-syntax.


try assertRefactorConvert(baseline, expected: expected)
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you add one test case where the try expression has a base indentation that needs to be applied to the do block? I think that would be valuable to test, ie. something like

func test() {
  try! print(
    "Hello"
  )
}

//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

😉

Suggested change
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors

Comment thread Sources/SwiftRefactor/ConvertToDoCatch.swift
@Clemo97 Clemo97 requested a review from ahoppen January 21, 2026 16:44
Copy link
Copy Markdown
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. Just a few more comments.

Comment thread Sources/SwiftRefactor/ConvertToDoCatch.swift Outdated
Comment on lines +109 to +113
// Format the do-catch statement with the proper indentation
let format = BasicFormat(
indentationWidth: indentWidth,
initialIndentation: baseIndentation
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This may change the user-provided formatting and I’d prefer to leave it as-is. Could you use Indenter instead to apply indentation to all code blocks that need indenting?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This may change the user-provided formatting and I’d prefer to leave it as-is. Could you use Indenter instead to apply indentation to all code blocks that need indenting?

Indenter in SwiftSyntaxBuilder has been deprecated in favor of the new indented(by:) in SwiftBasicFormat, so I used the newer .indented(by:) method.

try assertRefactorConvert(baseline, expected: expected)
}

func testBasicForceTryExpression() throws {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is exactly the same as testBasicForceTryConversion, right?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes, you are right, I've removed 'testBasicForceTryExpression'.

@ahoppen
Copy link
Copy Markdown
Member

ahoppen commented Feb 9, 2026

@Clemo97 Did you mean to push changes that addressed my last set of comments?

@ahoppen
Copy link
Copy Markdown
Member

ahoppen commented Feb 9, 2026

My last review comments are still not addressed in your latest version of the PR. Just thinking that you might have addressed them locally because you reacted to them.

@Clemo97
Copy link
Copy Markdown
Author

Clemo97 commented Feb 9, 2026

My last review comments are still not addressed in your latest version of the PR. Just thinking that you might have addressed them locally because you reacted to them.

Hello, sorry for the confusion I had forgotten to push my changes remotely, let me work on it.

Copy link
Copy Markdown
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.

Just one more small thing.

Comment thread Sources/SwiftRefactor/ConvertToDoCatch.swift
}

// Remove leading trivia from the try expression since we'll manage indentation
let trimmedTryExpression = tryExpression.with(\.leadingTrivia, [])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This would also remove any comments in the leading trivia. Maybe I’m missing something but I think you don’t need to remove the leading trivia here if you only the CodeBlockItemListSyntax by indentationWidth in line 83.

A test case for this would be the following, which should keep the comment.

// Hope this doesn’t explode
try! hopefullyNoBoom()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you add this test case to your PR?

Comment thread Tests/SwiftRefactorTest/ConvertToDoCatchTest.swift
@ahoppen
Copy link
Copy Markdown
Member

ahoppen commented Feb 21, 2026

Looks like you merged main into your branch but didn’t address my changes. Just want to make sure that you didn’t forget to push anything. Also, could you rebase and squash your changes instead of merging main into your branch, as described in https://github.com/swiftlang/sourcekit-lsp/blob/main/CONTRIBUTING.md#authoring-commits?

@Clemo97
Copy link
Copy Markdown
Author

Clemo97 commented Feb 24, 2026

Looks like you merged main into your branch but didn’t address my changes. Just want to make sure that you didn’t forget to push anything. Also, could you rebase and squash your changes instead of merging main into your branch, as described in https://github.com/swiftlang/sourcekit-lsp/blob/main/CONTRIBUTING.md#authoring-commits?

I accidentally closed the PR working on fixing it.

@ahoppen ahoppen reopened this Feb 25, 2026
Copy link
Copy Markdown
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.

I re-opened the PR. Two more small things.

Comment on lines +80 to +84
statements: CodeBlockItemListSyntax([
CodeBlockItemSyntax(
item: .expr(ExprSyntax(tryExprWithoutBaseIndent))
)
]).indented(by: baseIndentation + indentationWidth, indentFirstLine: true),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe I’m missing something but instead of stripping the base indentation and then adding it again, can’t this just be the following? At which point you don’t need stripBaseIndentation anymore.

Suggested change
statements: CodeBlockItemListSyntax([
CodeBlockItemSyntax(
item: .expr(ExprSyntax(tryExprWithoutBaseIndent))
)
]).indented(by: baseIndentation + indentationWidth, indentFirstLine: true),
statements: CodeBlockItemListSyntax([
CodeBlockItemSyntax(
item: .expr(ExprSyntax(tryExpression))
)
]).indented(by: indentationWidth, indentFirstLine: true),

}

// Remove leading trivia from the try expression since we'll manage indentation
let trimmedTryExpression = tryExpression.with(\.leadingTrivia, [])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you add this test case to your PR?

@Clemo97
Copy link
Copy Markdown
Author

Clemo97 commented Feb 25, 2026

I re-opened the PR. Two more small things.

Thanks

@ahoppen
Copy link
Copy Markdown
Member

ahoppen commented Mar 8, 2026

@Clemo97 I don’t see any recent changes made by you in this PR. Just want to make sure that you don’t have them implemented locally and just forgot to push them.

Copy link
Copy Markdown
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.

We recently decided that code actions should live in the SourceKit-LSP repository unless there is a good reason for it not to (https://github.com/swiftlang/sourcekit-lsp/blob/main/Contributor%20Documentation/Code%20Action.md). This means that we should implement this in the SourceKit-LSP repository. Could you open a PR to add it over there? That way you can also already include it in Code Actions.md and add it to the list of code actions reported by SourceKit-LSP.

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