Fix issue where redundantAsync ignored await keyword in string interpolation (#2225)

This commit is contained in:
Cal Stephens
2025-09-23 20:10:10 -07:00
parent 1c1d501068
commit d3cfc9df66
5 changed files with 45 additions and 10 deletions
+22 -5
View File
@@ -570,11 +570,6 @@ extension Formatter {
return false
}
// If the code is in a string, then it could be inside a string interpolation
if tokens[startOfScopeIndex] == .startOfScope("\"") || tokens[startOfScopeIndex] == .startOfScope("\"\"\"") {
return false
}
// If this is a function scope, but not the body of the function itself,
// then this is some nested function.
if lastSignificantKeyword(at: startOfScopeIndex, excluding: ["where"]) == "func",
@@ -587,6 +582,28 @@ extension Formatter {
return isInFunctionBody(of: functionDecl, at: startOfScopeIndex)
}
/// Whether or not the given index is within a string body or string interpolation
func isInStringInterpolation(at index: Int) -> Bool {
guard let startOfScopeIndex = startOfScope(at: index) else {
return false
}
// If the code is in a string, then it could be inside a string interpolation
if tokens[startOfScopeIndex] == .startOfScope("\"") || tokens[startOfScopeIndex] == .startOfScope("\"\"\"") {
return true
}
return isInStringInterpolation(at: startOfScopeIndex)
}
/// Whether or not the `try` keyword is supported at the given index
/// within the given function declaration, if it were throwing.
func tryKeywordSupported(at index: Int, in functionDecl: FunctionDeclaration) -> Bool {
isInFunctionBody(of: functionDecl, at: index)
// String interpolation is a non-throwing autoclosure, so can't use `try`
&& !isInStringInterpolation(at: index)
}
/// Whether or not this index the start of scope of a closure literal, eg `{` but not some other type of scope.
func isStartOfClosure(at i: Int) -> Bool {
guard token(at: i) == .startOfScope("{") else {
+1 -1
View File
@@ -37,7 +37,7 @@ public extension FormatRule {
// Only remove the `!` if we are not within a closure or nested function,
// where it's not safe to just remove the `!` and make our function throw.
guard formatter.isInFunctionBody(of: functionDecl, at: index) else { continue }
guard formatter.tryKeywordSupported(at: index, in: functionDecl) else { continue }
formatter.removeToken(at: nextTokenIndex)
foundAnyTryExclamationMarks = true
+4 -4
View File
@@ -53,7 +53,7 @@ public extension FormatRule {
}
// Only convert the `!` if we are within the function body
guard formatter.isInFunctionBody(of: functionDecl, at: forceUnwrapOperator.index) else {
guard formatter.tryKeywordSupported(at: forceUnwrapOperator.index, in: functionDecl) else {
continue
}
@@ -84,7 +84,7 @@ public extension FormatRule {
// Convert all eligible ! operators in this expression to ? operators
convertForceUnwrapsInExpression: for i in expressionRange.range.reversed() {
guard formatter.tokens[i] == .operator("!", .postfix),
formatter.isInFunctionBody(of: functionDecl, at: i)
formatter.tryKeywordSupported(at: i, in: functionDecl)
else { continue }
// Check if this force unwrap is in a function call or subscript call subexpression within this expression.
@@ -324,7 +324,7 @@ extension Formatter {
let firstInfixOperator = expressionRange.range.first(where: { i in
if treatAsInfixOperator(tokens[i], i),
let functionDecl,
isInFunctionBody(of: functionDecl, at: i),
tryKeywordSupported(at: i, in: functionDecl),
tokens[i] != .operator(".", .infix)
{
return true
@@ -340,7 +340,7 @@ extension Formatter {
let lhsFormatterOffset = expressionRange.lowerBound
guard let lhsForceUnwrapIndex = expressionRange.range.first(where: { i in
tokens[i] == .operator("!", .postfix) && isInFunctionBody(of: functionDecl, at: i) && i < infixIndex
tokens[i] == .operator("!", .postfix) && tryKeywordSupported(at: i, in: functionDecl) && i < infixIndex
}) else { return nil }
// Convert the absolute index to the sub-formatter's relative index
@@ -334,6 +334,7 @@ final class NoForceUnwrapInTestsTests: XCTestCase {
func test_closure() {
// Can't be try since string interpolation is a non-throwing autoclosure
print("foo \\(bar!)")
print("foo \\(foo!.bar!.baaz == quux)")
}
}
"""
+17
View File
@@ -277,4 +277,21 @@ class RedundantAsyncTests: XCTestCase {
let options = FormatOptions(redundantAsync: .always)
testFormatting(for: input, output, rule: .redundantAsync, options: options)
}
func testIssue2217_asyncNotRemovedForAwaitInStringInterpolation() {
let input = """
var x: String {
get async {
"y"
}
}
func y() async {
"\\(await x)"
}
"""
let options = FormatOptions(redundantAsync: .always)
testFormatting(for: input, rule: .redundantAsync, options: options)
}
}