Add ConvertToDoCatch refactoring to SwiftRefactor#3233
Add ConvertToDoCatch refactoring to SwiftRefactor#3233Clemo97 wants to merge 5 commits intoswiftlang:mainfrom
Conversation
| leftBrace: .leftBraceToken(trailingTrivia: .newline), | ||
| statements: CodeBlockItemListSyntax([ | ||
| CodeBlockItemSyntax( | ||
| leadingTrivia: .spaces(2), |
There was a problem hiding this comment.
Could we pass in the indentation of the source file through the context, similar to what we do in ExpandEditorPlaceholder?
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
Would it be easier to construct this statement using string interpolation? Ie.
let doStatement = DoStmtSyntax("""
do {
…
}
""")| try assertRefactorConvert(baseline, expected: expected) | ||
| } | ||
|
|
||
| func testForceTryWithVariableBinding() throws { |
There was a problem hiding this comment.
This test doesn’t contain a variable binding, right?
| 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) | ||
| } |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
This should already be covered by testForceTryWithClosure, which also contains a multi-line expression (which is what I think is relevant here)
| 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) | ||
| } |
There was a problem hiding this comment.
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) | ||
| } | ||
| } |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
😉
| // Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors | |
| // Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors |
ahoppen
left a comment
There was a problem hiding this comment.
Very nice. Just a few more comments.
| // Format the do-catch statement with the proper indentation | ||
| let format = BasicFormat( | ||
| indentationWidth: indentWidth, | ||
| initialIndentation: baseIndentation | ||
| ) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
This may change the user-provided formatting and I’d prefer to leave it as-is. Could you use
Indenterinstead 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 { |
There was a problem hiding this comment.
This is exactly the same as testBasicForceTryConversion, right?
There was a problem hiding this comment.
Yes, you are right, I've removed 'testBasicForceTryExpression'.
|
@Clemo97 Did you mean to push changes that addressed my last set of comments? |
|
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. |
| } | ||
|
|
||
| // Remove leading trivia from the try expression since we'll manage indentation | ||
| let trimmedTryExpression = tryExpression.with(\.leadingTrivia, []) |
There was a problem hiding this comment.
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()There was a problem hiding this comment.
Could you add this test case to your PR?
|
Looks like you merged |
I accidentally closed the PR working on fixing it. |
ahoppen
left a comment
There was a problem hiding this comment.
I re-opened the PR. Two more small things.
| statements: CodeBlockItemListSyntax([ | ||
| CodeBlockItemSyntax( | ||
| item: .expr(ExprSyntax(tryExprWithoutBaseIndent)) | ||
| ) | ||
| ]).indented(by: baseIndentation + indentationWidth, indentFirstLine: true), |
There was a problem hiding this comment.
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.
| 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, []) |
There was a problem hiding this comment.
Could you add this test case to your PR?
Thanks |
|
@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. |
…dentation management
ahoppen
left a comment
There was a problem hiding this comment.
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.
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