Skip to content

Commit 09c1295

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 a36b6c0 commit 09c1295

File tree

7 files changed

+988
-4
lines changed

7 files changed

+988
-4
lines changed

Package.swift

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

251251
.target(
252252
name: "SwiftRefactor",
253-
dependencies: ["SwiftParser", "SwiftSyntax"]
253+
dependencies: ["SwiftBasicFormat", "SwiftParser", "SwiftSyntax", "SwiftSyntaxBuilder"]
254254
),
255255

256256
.testTarget(
257257
name: "SwiftRefactorTest",
258-
dependencies: ["_SwiftSyntaxTestSupport", "SwiftRefactor", "SwiftSyntaxBuilder"]
258+
dependencies: ["_SwiftSyntaxTestSupport", "SwiftRefactor"]
259259
),
260260

261261
// MARK: - Executable targets
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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(
84+
\.leadingTrivia,
85+
Trivia()
86+
.merging(triviaOf: closures.first!.original.label)
87+
.merging(triviaOf: closures.first!.original.colon)
88+
.merging(closures.first!.closure.leadingTrivia)
89+
)
90+
let additionalTrailingClosures = closures.dropFirst().map {
91+
MultipleTrailingClosureElementSyntax(
92+
label: $0.original.label ?? .wildcardToken(),
93+
colon: $0.original.colon ?? .colonToken(),
94+
closure: $0.closure
95+
)
96+
}
97+
98+
var converted = self.detach()
99+
100+
// Remove parens if there's no non-closure arguments left and remove the
101+
// last comma otherwise. Makes sure to keep the trivia of any removed node.
102+
var argList = Array(argumentList.dropLast(closures.count))
103+
if argList.isEmpty {
104+
converted =
105+
converted
106+
.with(\.leftParen, nil)
107+
.with(\.rightParen, nil)
108+
109+
// No left paren any more, right paren is handled below since it makes
110+
// sense to keep its trivia of the end of the call, regardless of whether
111+
// it was removed or not.
112+
if let leftParen = leftParen {
113+
trailingClosure = trailingClosure.with(
114+
\.leadingTrivia,
115+
Trivia()
116+
.merging(triviaOf: leftParen)
117+
.merging(trailingClosure.leadingTrivia)
118+
)
119+
}
120+
} else {
121+
let last = argList.last!
122+
if let comma = last.trailingComma {
123+
converted =
124+
converted
125+
.with(\.rightParen, TokenSyntax.rightParenToken(trailingTrivia: Trivia().merging(triviaOf: comma)))
126+
}
127+
argList[argList.count - 1] =
128+
last
129+
.with(\.trailingComma, nil)
130+
}
131+
132+
// Update arguments and trailing closures
133+
converted =
134+
converted
135+
.with(\.argumentList, TupleExprElementListSyntax(argList))
136+
.with(\.trailingClosure, trailingClosure)
137+
if !additionalTrailingClosures.isEmpty {
138+
converted = converted.with(\.additionalTrailingClosures, MultipleTrailingClosureElementListSyntax(additionalTrailingClosures))
139+
}
140+
141+
// The right paren either doesn't exist any more, or is before all the
142+
// trailing closures. Moves its trivia to the end of the converted call.
143+
if let rightParen = rightParen {
144+
converted = converted.with(\.trailingTrivia, converted.trailingTrivia.merging(triviaOf: rightParen))
145+
}
146+
147+
return converted
148+
}
149+
}

0 commit comments

Comments
 (0)