diff --git a/Rules.md b/Rules.md index 9f975982..2ffc75d3 100644 --- a/Rules.md +++ b/Rules.md @@ -83,6 +83,7 @@ * [blankLinesBetweenImports](#blankLinesBetweenImports) * [blockComments](#blockComments) * [docComments](#docComments) +* [hoistAwait](#hoistAwait) * [isEmpty](#isEmpty) * [markTypes](#markTypes) * [organizeDeclarations](#organizeDeclarations) @@ -747,6 +748,10 @@ Option | Description
+## hoistAwait + +Reposition `await` keyword outside of the current scope. + ## hoistPatternLet Reposition `let` or `var` bindings within pattern. diff --git a/Sources/Rules.swift b/Sources/Rules.swift index 30c5b7bd..aafd6876 100644 --- a/Sources/Rules.swift +++ b/Sources/Rules.swift @@ -4171,6 +4171,76 @@ public struct _FormatRules { } } + /// Reposition `await` keyword outside of the current scope. + public let hoistAwait = FormatRule( + help: "Reposition `await` keyword outside of the current scope.", + disabledByDefault: true, + options: [] + ) { formatter in + formatter.forEach(.startOfScope("(")) { idx, _ in + func insertAwait(at insertIndex: Int) { + let awaitIndex = formatter.index(of: .keyword("await"), before: idx) + + guard awaitIndex == nil || formatter.tokens[awaitIndex! ..< idx].contains( + where: { $0.isStartOfScope || $0.isLinebreak } + ) else { + return + } + + formatter.insert([.keyword("await")], at: insertIndex) + + if formatter.token(at: insertIndex + 1)?.isSpace == false { + formatter.insertSpace(" ", at: insertIndex + 1) + } + + if insertIndex > 0, + let previousToken = formatter.token(at: insertIndex - 1), + !previousToken.isLinebreak, + previousToken.isSpace == false + { + formatter.insertSpace(" ", at: insertIndex) + } + } + + guard let endIndex = formatter.index(of: .endOfScope(")"), after: idx), + formatter.lastSignificantKeyword(at: idx) != "if" + else { + return + } + + var awaitIndexes: [Int] = [] + var index = idx + while let next = formatter.index(of: .keyword("await"), after: index), index < endIndex { + awaitIndexes.append(next) + index = next + } + + guard !awaitIndexes.isEmpty else { return } + + let prevIndex = formatter.index(before: idx, where: { + $0.isSpaceOrLinebreak + }) + + awaitIndexes.reversed().forEach { awaitIndex in + formatter.removeToken(at: awaitIndex) + if formatter.tokens[awaitIndex].isSpace == true { + formatter.removeToken(at: awaitIndex) + } + } + + if let prevIndex = prevIndex { + let token = formatter.token(at: prevIndex) + if token?.isLinebreak == true { + return insertAwait(at: prevIndex + 1) + } else { + return insertAwait(at: prevIndex) + } + } else { + insertAwait(at: 0) + } + } + } + /// Move `let` and `var` inside patterns to the beginning public let hoistPatternLet = FormatRule( help: "Reposition `let` or `var` bindings within pattern.", diff --git a/Tests/RulesTests+Syntax.swift b/Tests/RulesTests+Syntax.swift index 21de01d1..e44d743e 100644 --- a/Tests/RulesTests+Syntax.swift +++ b/Tests/RulesTests+Syntax.swift @@ -604,6 +604,76 @@ class SyntaxTests: RulesTests { testFormatting(for: input, rule: FormatRules.trailingClosures) } + // MARK: - hoistAwait + + func testHoistAwait() { + let input = "greet(await name, await surname)" + let output = "await greet(name, surname)" + testFormatting(for: input, output, rule: FormatRules.hoistAwait) + } + + func testHoistAwaitInsideIfDoesNothing() { + let input = "if !(await isSomething()) {}" + testFormatting(for: input, rule: FormatRules.hoistAwait) + } + + func testHoistAwaitInsideArgument() { + let input = """ + array.append(contentsOf: try await asyncFunction(param1: param1)) + """ + let output = """ + await array.append(contentsOf: try asyncFunction(param1: param1)) + """ + testFormatting(for: input, output, rule: FormatRules.hoistAwait) + } + + func testHoistAwaitWithReturn() { + let input = "return .enumCase(try await service.greet())" + let output = "return await .enumCase(try service.greet())" + testFormatting(for: input, output, rule: FormatRules.hoistAwait) + } + + func testHoistAwaitWithInitAssignment() { + let input = "let variable = String(try await asyncFunction())" + let output = "let variable = await String(try asyncFunction())" + testFormatting(for: input, output, rule: FormatRules.hoistAwait) + } + + func testHoistAwaitWithAssignment() { + let input = "let variable = (try await asyncFunction())" + let output = "let variable = await (try asyncFunction())" + testFormatting(for: input, output, rule: FormatRules.hoistAwait) + } + + func testHoistAwaitInRedundantScopePriorToNumber() { + let input = """ + let identifiersTypes = 1 + (try? await asyncFunction(param1: param1)) + """ + let output = """ + let identifiersTypes = 1 + await (try? asyncFunction(param1: param1)) + """ + testFormatting(for: input, output, rule: FormatRules.hoistAwait) + } + + func testHoistAwaitOnlyOne() { + let input = "greet(name, await surname)" + let output = "await greet(name, surname)" + testFormatting(for: input, output, rule: FormatRules.hoistAwait) + } + + func testHoistAwaitRedundantAwait() { + let input = "await greet(await name, await surname)" + let output = "await greet(name, surname)" + testFormatting(for: input, output, rule: FormatRules.hoistAwait) + } + + func testHoistAwaitDoesNothing() { + let input = "await greet(name, surname)" + testFormatting(for: input, rule: FormatRules.hoistAwait) + } + // MARK: - hoistPatternLet // hoist = true