Skip to content

Commit 303c058

Browse files
committed
Add convert to trailing closure and editor placeholder refactorings
Adds three new refactorings: - `ConvertToTrailingClosures` - `ExpandEditorPlaceholder` - `ExpandEditorPlaceholders` `ExpandEditorPlaceholders` is a combination of `ExpandEditorPlaceholder` and `ConvertToTrailingClosures`, ie. it first expands any function-typed closures at the end of a call using `ExpandEditorPlaceholder` and then runs `ConvertToTrailingClosures` on that call. Resolves rdar://107532856.
1 parent 5f86148 commit 303c058

File tree

7 files changed

+969
-4
lines changed

7 files changed

+969
-4
lines changed

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,12 +247,12 @@ let package = Package(
247247

248248
.target(
249249
name: "SwiftRefactor",
250-
dependencies: ["SwiftParser", "SwiftSyntax"]
250+
dependencies: ["SwiftBasicFormat", "SwiftParser", "SwiftSyntax", "SwiftSyntaxBuilder"]
251251
),
252252

253253
.testTarget(
254254
name: "SwiftRefactorTest",
255-
dependencies: ["_SwiftSyntaxTestSupport", "SwiftRefactor", "SwiftSyntaxBuilder"]
255+
dependencies: ["_SwiftSyntaxTestSupport", "SwiftRefactor"]
256256
),
257257

258258
// MARK: - Executable targets
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftBasicFormat
14+
import SwiftSyntax
15+
16+
/// Convert a call with inline closures to one that uses trailing closure
17+
/// syntax. Returns `nil` if there's already trailing closures or there are no
18+
/// closures within the call. Pass `startAtArgument` to specify the argument
19+
/// index to start the conversion from, ie. to skip converting closures before
20+
/// `startAtArgument`.
21+
///
22+
/// ## Before
23+
/// ```
24+
/// someCall(closure1: { arg in
25+
/// return 1
26+
/// }, closure2: { arg in
27+
/// return 2
28+
/// })
29+
/// ```
30+
///
31+
/// ## After
32+
/// ```
33+
/// someCall { arg in
34+
/// return 1
35+
/// } closure2: { arg in
36+
/// return 2
37+
/// }
38+
/// ```
39+
public struct CallToTrailingClosures: SyntaxRefactoringProvider {
40+
public struct Context {
41+
public let startAtArgument: Int
42+
43+
public init(startAtArgument: Int = 0) {
44+
self.startAtArgument = startAtArgument
45+
}
46+
}
47+
48+
// TODO: Rather than returning nil, we should consider throwing errors with
49+
// appropriate messages instead.
50+
public static func refactor(syntax call: FunctionCallExprSyntax, in context: Context = Context()) -> FunctionCallExprSyntax? {
51+
return call.convertToTrailingClosures(from: context.startAtArgument)?.formatted().as(FunctionCallExprSyntax.self)
52+
}
53+
}
54+
55+
extension FunctionCallExprSyntax {
56+
fileprivate func convertToTrailingClosures(from startAtArgument: Int) -> FunctionCallExprSyntax? {
57+
guard trailingClosure == nil, additionalTrailingClosures == nil, leftParen != nil, rightParen != nil else {
58+
// Already have trailing closures
59+
return nil
60+
}
61+
62+
var closures = [(original: TupleExprElementSyntax, closure: ClosureExprSyntax)]()
63+
for arg in argumentList.dropFirst(startAtArgument) {
64+
guard var closure = arg.expression.as(ClosureExprSyntax.self) else {
65+
closures.removeAll()
66+
continue
67+
}
68+
69+
// Trailing comma won't exist any more, move its trivia to the end of
70+
// the closure instead
71+
if let comma = arg.trailingComma {
72+
closure = closure.with(\.trailingTrivia, closure.trailingTrivia.merging(triviaOf: comma))
73+
}
74+
closures.append((arg, closure))
75+
}
76+
77+
guard !closures.isEmpty else {
78+
return nil
79+
}
80+
81+
// First trailing closure won't have label/colon. Transfer their trivia.
82+
var trailingClosure = closures.first!.closure
83+
.with(\.leadingTrivia, Trivia()
84+
.merging(triviaOf: closures.first!.original.label)
85+
.merging(triviaOf: closures.first!.original.colon)
86+
.merging(closures.first!.closure.leadingTrivia))
87+
let additionalTrailingClosures = closures.dropFirst().map {
88+
MultipleTrailingClosureElementSyntax(
89+
label: $0.original.label ?? .wildcardToken(),
90+
colon: $0.original.colon ?? .colonToken(),
91+
closure: $0.closure
92+
)
93+
}
94+
95+
var converted = self.detach()
96+
97+
// Remove parens if there's no non-closure arguments left and remove the
98+
// last comma otherwise. Makes sure to keep the trivia of any removed node.
99+
var argList = Array(argumentList.dropLast(closures.count))
100+
if argList.isEmpty {
101+
converted =
102+
converted
103+
.with(\.leftParen, nil)
104+
.with(\.rightParen, nil)
105+
106+
// No left paren any more, right paren is handled below since it makes
107+
// sense to keep its trivia of the end of the call, regardless of whether
108+
// it was removed or not.
109+
if let leftParen = leftParen {
110+
trailingClosure = trailingClosure.with(\.leadingTrivia, Trivia()
111+
.merging(triviaOf: leftParen)
112+
.merging(trailingClosure.leadingTrivia))
113+
}
114+
} else {
115+
let last = argList.last!
116+
if let comma = last.trailingComma {
117+
converted =
118+
converted
119+
.with(\.rightParen, TokenSyntax.rightParenToken(trailingTrivia: Trivia().merging(triviaOf: comma)))
120+
}
121+
argList[argList.count - 1] =
122+
last
123+
.with(\.trailingComma, nil)
124+
}
125+
126+
// Update arguments and trailing closures
127+
converted =
128+
converted
129+
.with(\.argumentList, TupleExprElementListSyntax(argList))
130+
.with(\.trailingClosure, trailingClosure)
131+
if !additionalTrailingClosures.isEmpty {
132+
converted = converted.with(\.additionalTrailingClosures, MultipleTrailingClosureElementListSyntax(additionalTrailingClosures))
133+
}
134+
135+
// The right paren either doesn't exist any more, or is before all the
136+
// trailing closures. Moves its trivia to the end of the converted call.
137+
if let rightParen = rightParen {
138+
converted = converted.with(\.trailingTrivia, converted.trailingTrivia.merging(triviaOf: rightParen))
139+
}
140+
141+
return converted
142+
}
143+
}

0 commit comments

Comments
 (0)