diff --git a/Sources/ParsingHelpers.swift b/Sources/ParsingHelpers.swift index cd0d41fa..966c5036 100644 --- a/Sources/ParsingHelpers.swift +++ b/Sources/ParsingHelpers.swift @@ -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 { diff --git a/Sources/Rules/NoForceTryInTests.swift b/Sources/Rules/NoForceTryInTests.swift index 1e27d28a..0a1c7c0f 100644 --- a/Sources/Rules/NoForceTryInTests.swift +++ b/Sources/Rules/NoForceTryInTests.swift @@ -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 diff --git a/Sources/Rules/NoForceUnwrapInTests.swift b/Sources/Rules/NoForceUnwrapInTests.swift index d301e022..384ed5b0 100644 --- a/Sources/Rules/NoForceUnwrapInTests.swift +++ b/Sources/Rules/NoForceUnwrapInTests.swift @@ -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 diff --git a/Tests/Rules/NoForceUnwrapInTestsTests.swift b/Tests/Rules/NoForceUnwrapInTestsTests.swift index bc0a256e..28cb2232 100644 --- a/Tests/Rules/NoForceUnwrapInTestsTests.swift +++ b/Tests/Rules/NoForceUnwrapInTestsTests.swift @@ -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)") } } """ diff --git a/Tests/Rules/RedundantAsyncTests.swift b/Tests/Rules/RedundantAsyncTests.swift index 8bf63f47..af2e437e 100644 --- a/Tests/Rules/RedundantAsyncTests.swift +++ b/Tests/Rules/RedundantAsyncTests.swift @@ -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) + } }