mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
188 lines
8.7 KiB
Swift
188 lines
8.7 KiB
Swift
//
|
|
// TrailingClosures.swift
|
|
// SwiftFormat
|
|
//
|
|
// Created by Nick Lockwood on 1/17/17.
|
|
// Copyright © 2024 Nick Lockwood. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
public extension FormatRule {
|
|
/// Convert closure arguments to trailing closure syntax where possible
|
|
static let trailingClosures = FormatRule(
|
|
help: "Use trailing closure syntax where applicable.",
|
|
options: ["trailing-closures", "never-trailing"]
|
|
) { formatter in
|
|
let useTrailing = Set([
|
|
"async", "asyncAfter", "sync", "autoreleasepool",
|
|
] + formatter.options.trailingClosures)
|
|
|
|
let nonTrailing = Set([
|
|
"performBatchUpdates",
|
|
"expect", // Special case to support autoclosure arguments in the Nimble framework
|
|
] + formatter.options.neverTrailing)
|
|
|
|
formatter.forEach(.startOfScope("(")) { functionOpenParen, _ in
|
|
guard let identifierIndex = formatter.parseFunctionIdentifier(beforeStartOfScope: functionOpenParen) else { return }
|
|
let name = formatter.tokens[identifierIndex].string
|
|
|
|
guard !nonTrailing.contains(name), !formatter.isConditionalStatement(at: functionOpenParen),
|
|
!formatter.isAttribute(at: identifierIndex)
|
|
else {
|
|
return
|
|
}
|
|
|
|
// Parse all arguments to detect multiple trailing closures
|
|
let arguments = formatter.parseFunctionCallArguments(startOfScope: functionOpenParen)
|
|
|
|
let trailingClosures = arguments.suffix(while: { arg in
|
|
let range = arg.valueRange
|
|
guard let first = formatter.index(of: .nonSpaceOrCommentOrLinebreak, in: range.lowerBound ..< range.upperBound + 1),
|
|
let last = formatter.index(of: .nonSpaceOrCommentOrLinebreak, before: range.upperBound + 1, if: { _ in true }),
|
|
formatter.tokens[first] == .startOfScope("{"),
|
|
formatter.tokens[last] == .endOfScope("}"),
|
|
formatter.index(of: .endOfScope("}"), after: first) == last
|
|
else {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|
|
// Ensure the function doesn't already have a trailing closure
|
|
guard let functionCallClosingParen = formatter.endOfScope(at: functionOpenParen),
|
|
formatter.next(.nonSpaceOrCommentOrLinebreak, after: functionCallClosingParen) != .startOfScope("{")
|
|
else { return }
|
|
|
|
// Handle a single trailing closure
|
|
if trailingClosures.count == 1 {
|
|
guard trailingClosures[0].label == nil || useTrailing.contains(name) else { return }
|
|
|
|
let closure = trailingClosures[0]
|
|
let range = closure.valueRange
|
|
guard let closingBraceIndex = formatter.index(of: .nonSpaceOrComment, before: functionCallClosingParen, if: { $0 == .endOfScope("}") }),
|
|
let openingBraceIndex = formatter.index(of: .startOfScope("{"), before: closingBraceIndex),
|
|
formatter.index(of: .endOfScope("}"), before: openingBraceIndex) == nil,
|
|
var startIndex = formatter.index(of: .nonSpaceOrLinebreak, before: openingBraceIndex)
|
|
else { return }
|
|
|
|
switch formatter.tokens[startIndex] {
|
|
case .delimiter(","), .startOfScope("("):
|
|
break
|
|
case .delimiter(":"):
|
|
if let commaIndex = formatter.index(of: .delimiter(","), before: openingBraceIndex) {
|
|
startIndex = commaIndex
|
|
} else if formatter.index(of: .startOfScope("("), before: openingBraceIndex) == functionOpenParen {
|
|
startIndex = functionOpenParen
|
|
} else {
|
|
return
|
|
}
|
|
default:
|
|
return
|
|
}
|
|
let wasParen = (startIndex == functionOpenParen)
|
|
formatter.removeParen(at: functionCallClosingParen)
|
|
formatter.replaceTokens(in: startIndex ..< openingBraceIndex, with:
|
|
wasParen ? [.space(" ")] : [.endOfScope(")"), .space(" ")])
|
|
return
|
|
}
|
|
|
|
else if trailingClosures.count >= 2 {
|
|
guard trailingClosures[0].label == nil,
|
|
trailingClosures.dropFirst().allSatisfy({ $0.label != nil })
|
|
else { return }
|
|
|
|
// Remove the closing paren and any trailing comma.
|
|
// If the closing paren is on its own line, remove the whole line.
|
|
let lineWithClosingParen = formatter.startOfLine(at: functionCallClosingParen) ... formatter.endOfLine(at: functionCallClosingParen)
|
|
let tokenBeforeClosingParen = formatter.index(of: .nonSpaceOrCommentOrLinebreak, before: functionCallClosingParen)
|
|
|
|
if formatter.tokens(in: lineWithClosingParen)?.string.trimmingCharacters(in: .whitespacesAndNewlines) == ")" {
|
|
formatter.removeTokens(in: lineWithClosingParen)
|
|
} else {
|
|
formatter.removeToken(at: functionCallClosingParen)
|
|
}
|
|
|
|
if let tokenBeforeClosingParen, formatter.tokens[tokenBeforeClosingParen] == .delimiter(",") {
|
|
formatter.removeToken(at: tokenBeforeClosingParen)
|
|
}
|
|
|
|
// Remove the comma before each closure
|
|
for (index, closure) in trailingClosures.enumerated().reversed() {
|
|
let closureRange = closure.valueRange.autoUpdating(in: formatter)
|
|
let closureOnOneLine = formatter.onSameLine(closureRange.lowerBound, closureRange.upperBound)
|
|
|
|
if index == trailingClosures.indices.first,
|
|
let indexBeforeFirstClosure = formatter.index(of: .nonSpaceOrCommentOrLinebreak, before: closure.valueRange.lowerBound)
|
|
{
|
|
// If all of the arguments were closures, remove the parens completely
|
|
if formatter.tokens[indexBeforeFirstClosure] == .startOfScope("(") {
|
|
formatter.removeToken(at: indexBeforeFirstClosure)
|
|
}
|
|
|
|
// Otherwise, the previous comma becomes the new closing paren
|
|
else if formatter.tokens[indexBeforeFirstClosure] == .delimiter(",") {
|
|
formatter.replaceToken(at: indexBeforeFirstClosure, with: .endOfScope(")"))
|
|
}
|
|
|
|
// Unwrap the line so the trailing closure label immediately follows the closing paren
|
|
if !formatter.onSameLine(indexBeforeFirstClosure, closure.valueRange.lowerBound) {
|
|
formatter.unwrapLine(before: closureRange.lowerBound, preservingComments: true)
|
|
}
|
|
}
|
|
|
|
else if let closureLabelIndex = closure.labelIndex?.autoUpdating(in: formatter),
|
|
let commaIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, before: closureLabelIndex),
|
|
formatter.tokens[commaIndex] == .delimiter(",")
|
|
{
|
|
let commaWasOnPreviousLine = !formatter.onSameLine(commaIndex, closureLabelIndex.index)
|
|
formatter.removeToken(at: commaIndex)
|
|
|
|
// Unwrap the line so the trailing closure label immediately follows the previous end of scope
|
|
if commaWasOnPreviousLine {
|
|
formatter.unwrapLine(before: closureLabelIndex.index, preservingComments: true)
|
|
}
|
|
}
|
|
|
|
// If the closure was originally written on a single line, wrap it now.
|
|
if closureOnOneLine {
|
|
formatter.wrapLine(before: closureRange.upperBound)
|
|
formatter.wrapLine(before: closureRange.lowerBound + 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} examples: {
|
|
"""
|
|
```diff
|
|
- DispatchQueue.main.async(execute: { ... })
|
|
+ DispatchQueue.main.async {
|
|
```
|
|
|
|
```diff
|
|
- let foo = bar.map({ ... }).joined()
|
|
+ let foo = bar.map { ... }.joined()
|
|
```
|
|
|
|
```diff
|
|
- withAnimation(.spring, {
|
|
- isVisible = true
|
|
- }, completion: {
|
|
- handleCompletion()
|
|
- })
|
|
+ withAnimation(.spring) {
|
|
+ isVisible = true
|
|
+ } completion: {
|
|
+ handleCompletion()
|
|
+ }
|
|
```
|
|
"""
|
|
}
|
|
}
|
|
|
|
extension Collection {
|
|
func suffix(while condition: (Element) -> Bool) -> [Element] {
|
|
reversed().prefix(while: condition).reversed()
|
|
}
|
|
}
|