Skip to content

Commit d1765f8

Browse files
committed
Fix trivia handling issues in editor placeholder expansion
rdar://123287930
1 parent 58ef942 commit d1765f8

File tree

9 files changed

+307
-39
lines changed

9 files changed

+307
-39
lines changed

Release Notes/600.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@
5353
- Description: `macroSpecs` can have additional specifications like conformances provided by member or extension macro that can be used for macro expansion.
5454
- Issue: https://github.com/apple/swift-syntax/issues/2031
5555
- Pull Request: https://github.com/apple/swift-syntax/pull/2327
56+
57+
- `BasicFormat.reset`
58+
- Description: `BasicFormat` stores internal state while it formats a syntax tree. If the same `BasicFormat` instance is used to format multiple syntax trees, the initial state nees to be reset between calls to `BasicFormat.rewrite`. `SyntaxProtocol.formatted()` automatically handles this.
59+
- Pull Request: https://github.com/apple/swift-syntax/pull/2511
5660

5761
## API Behavior Changes
5862

Sources/SwiftBasicFormat/BasicFormat.swift

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ open class BasicFormat: SyntaxRewriter {
6969
super.init(viewMode: viewMode)
7070
}
7171

72+
/// Clears all stateful data from this `BasicFormat`.
73+
///
74+
/// This needs to be called between multiple `rewrite` calls to a syntax tree.
75+
public func reset() {
76+
indentationStack = [indentationStack.first!]
77+
anchorPoints = [:]
78+
previousToken = nil
79+
stringLiteralNestingLevel = 0
80+
}
81+
7282
// MARK: - Updating indentation level
7383

7484
public func increaseIndentationLevel(to userDefinedIndentation: Trivia? = nil) {
@@ -145,7 +155,7 @@ open class BasicFormat: SyntaxRewriter {
145155

146156
/// Find the indentation of the nearest ancestor whose first token is an
147157
/// anchor point (see `anchorPoints`).
148-
private func anchorPointIndentation(for token: TokenSyntax) -> Trivia? {
158+
private func anchorPointIndentation(for token: TokenSyntax) -> Trivia {
149159
var ancestor: Syntax = Syntax(token)
150160
while let parent = ancestor.parent {
151161
ancestor = parent
@@ -155,7 +165,7 @@ open class BasicFormat: SyntaxRewriter {
155165
return anchorPointIndentation
156166
}
157167
}
158-
return nil
168+
return Trivia()
159169
}
160170

161171
// MARK: - Customization points
@@ -575,23 +585,24 @@ open class BasicFormat: SyntaxRewriter {
575585
trailingTrivia += .space
576586
}
577587

578-
var leadingTriviaIndentation = self.currentIndentationLevel
579-
var trailingTriviaIndentation = self.currentIndentationLevel
588+
let leadingTriviaIndentation: Trivia
589+
let trailingTriviaIndentation: Trivia
580590

581591
// If the trivia contains user-defined indentation, find their anchor point
582592
// and indent the token relative to that anchor point.
593+
//
583594
// Always indent string literals relative to their anchor point because
584595
// their indentation has structural meaning and we just want to maintain
585596
// what the user wrote.
586-
if leadingTrivia.containsIndentation(isOnNewline: previousTokenWillEndWithNewline) || isInsideStringLiteral,
587-
let anchorPointIndentation = self.anchorPointIndentation(for: token)
588-
{
589-
leadingTriviaIndentation = anchorPointIndentation
597+
if leadingTrivia.containsIndentation(isOnNewline: previousTokenWillEndWithNewline) || isInsideStringLiteral {
598+
leadingTriviaIndentation = anchorPointIndentation(for: token)
599+
} else {
600+
leadingTriviaIndentation = currentIndentationLevel
590601
}
591-
if combinedTrailingTrivia.containsIndentation(isOnNewline: previousTokenWillEndWithNewline) || isInsideStringLiteral,
592-
let anchorPointIndentation = self.anchorPointIndentation(for: token)
593-
{
594-
trailingTriviaIndentation = anchorPointIndentation
602+
if combinedTrailingTrivia.containsIndentation(isOnNewline: previousTokenWillEndWithNewline) {
603+
trailingTriviaIndentation = anchorPointIndentation(for: token)
604+
} else {
605+
trailingTriviaIndentation = currentIndentationLevel
595606
}
596607

597608
leadingTrivia = leadingTrivia.indented(

Sources/SwiftBasicFormat/Syntax+Extensions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ extension TokenSyntax {
1919
/// find the indentation of the line this token occurs on.
2020
private var indentation: Trivia? {
2121
let previous = self.previousToken(viewMode: .sourceAccurate)
22-
return ((previous?.trailingTrivia ?? []) + leadingTrivia).indentation(isOnNewline: false)
22+
return ((previous?.trailingTrivia ?? []) + leadingTrivia).indentation(isOnNewline: previous == nil)
2323
}
2424

2525
/// Returns the indentation of the line this token occurs on

Sources/SwiftBasicFormat/SyntaxProtocol+Formatted.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import SwiftSyntax
1515
public extension SyntaxProtocol {
1616
/// Build a syntax node from this `Buildable` and format it with the given format.
1717
func formatted(using format: BasicFormat = BasicFormat()) -> Syntax {
18+
format.reset()
1819
return format.rewrite(self)
1920
}
2021
}

Sources/SwiftRefactor/CallToTrailingClosures.swift

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ public struct CallToTrailingClosures: SyntaxRefactoringProvider {
4848
// TODO: Rather than returning nil, we should consider throwing errors with
4949
// appropriate messages instead.
5050
public static func refactor(syntax call: FunctionCallExprSyntax, in context: Context = Context()) -> FunctionCallExprSyntax? {
51-
return call.convertToTrailingClosures(from: context.startAtArgument)?.formatted().as(FunctionCallExprSyntax.self)
51+
let converted = call.convertToTrailingClosures(from: context.startAtArgument)
52+
return converted?.formatted().as(FunctionCallExprSyntax.self)
5253
}
5354
}
5455

@@ -80,10 +81,12 @@ extension FunctionCallExprSyntax {
8081

8182
// First trailing closure won't have label/colon. Transfer their trivia.
8283
var trailingClosure = closures.first!.closure
83-
trailingClosure.leadingTrivia = Trivia()
84+
trailingClosure.leadingTrivia =
85+
Trivia()
8486
.merging(triviaOf: closures.first!.original.label)
8587
.merging(triviaOf: closures.first!.original.colon)
8688
.merging(closures.first!.closure.leadingTrivia)
89+
.droppingLeadingWhitespace
8790
let additionalTrailingClosures = closures.dropFirst().map {
8891
MultipleTrailingClosureElementSyntax(
8992
label: $0.original.label ?? .wildcardToken(),
@@ -94,6 +97,9 @@ extension FunctionCallExprSyntax {
9497

9598
var converted = self.detached
9699

100+
// Trivia that should be attached to the end of the converted call.
101+
var additionalTriviaAtEndOfCall: Trivia? = nil
102+
97103
// Remove parens if there's no non-closure arguments left and remove the
98104
// last comma otherwise. Makes sure to keep the trivia of any removed node.
99105
var argList = Array(arguments.dropLast(closures.count))
@@ -109,11 +115,17 @@ extension FunctionCallExprSyntax {
109115
.merging(triviaOf: leftParen)
110116
.merging(trailingClosure.leadingTrivia)
111117
}
118+
// No right paren anymore. Attach its trivia to the end of the call.
119+
if let rightParen = rightParen {
120+
additionalTriviaAtEndOfCall = Trivia().merging(triviaOf: rightParen)
121+
}
112122
} else {
113123
let last = argList.last!
114-
if let comma = last.trailingComma {
115-
converted.rightParen = TokenSyntax.rightParenToken(trailingTrivia: Trivia().merging(triviaOf: comma))
116-
}
124+
// Move the trailing trivia of the closing parenthesis to the end of the call after the last trailing, instead of
125+
// keeping it in the middle of the call where the new closing parenthesis lives.
126+
// Also ensure that we don't drop trivia from any comma we remove.
127+
converted.rightParen?.trailingTrivia = Trivia().merging(triviaOf: last.trailingComma)
128+
additionalTriviaAtEndOfCall = rightParen?.trailingTrivia
117129
argList[argList.count - 1] = last.with(\.trailingComma, nil)
118130
}
119131

@@ -124,12 +136,22 @@ extension FunctionCallExprSyntax {
124136
converted.additionalTrailingClosures = MultipleTrailingClosureElementListSyntax(additionalTrailingClosures)
125137
}
126138

127-
// The right paren either doesn't exist any more, or is before all the
128-
// trailing closures. Moves its trivia to the end of the converted call.
129-
if let rightParen = rightParen {
130-
converted.trailingTrivia = converted.trailingTrivia.merging(triviaOf: rightParen)
139+
if let additionalTriviaAtEndOfCall {
140+
converted.trailingTrivia = converted.trailingTrivia.merging(additionalTriviaAtEndOfCall.droppingLeadingWhitespace)
131141
}
132142

133143
return converted
134144
}
135145
}
146+
147+
fileprivate extension Trivia {
148+
var droppingLeadingWhitespace: Trivia {
149+
return Trivia(pieces: self.drop(while: \.isWhitespace))
150+
}
151+
}
152+
153+
fileprivate extension Sequence {
154+
func dropSuffix(while predicate: (Element) -> Bool) -> [Element] {
155+
self.reversed().drop(while: predicate).reversed()
156+
}
157+
}

Sources/SwiftRefactor/ExpandEditorPlaceholder.swift

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,17 @@ import SwiftSyntaxBuilder
7070
/// anything here
7171
/// ```
7272
struct ExpandSingleEditorPlaceholder: EditRefactoringProvider {
73-
static func textRefactor(syntax token: TokenSyntax, in context: Void) -> [SourceEdit] {
73+
struct Context {
74+
let indentationWidth: Trivia?
75+
let initialIndentation: Trivia
76+
77+
init(indentationWidth: Trivia? = nil, initialIndentation: Trivia = []) {
78+
self.indentationWidth = indentationWidth
79+
self.initialIndentation = initialIndentation
80+
}
81+
}
82+
83+
static func textRefactor(syntax token: TokenSyntax, in context: Context = Context()) -> [SourceEdit] {
7484
guard let placeholder = EditorPlaceholderData(token: token) else {
7585
return []
7686
}
@@ -81,7 +91,18 @@ struct ExpandSingleEditorPlaceholder: EditRefactoringProvider {
8191
expanded = String(text)
8292
case let .typed(text, type):
8393
if let functionType = type.as(FunctionTypeSyntax.self) {
84-
expanded = functionType.closureExpansion.formatted().description
94+
let basicFormat = BasicFormat(
95+
indentationWidth: context.indentationWidth,
96+
initialIndentation: context.initialIndentation
97+
)
98+
var formattedExpansion = functionType.closureExpansion.formatted(using: basicFormat).description
99+
// Strip the initial indentation from the placeholder itself. We only introduced the initial indentation to
100+
// format consecutive lines. We don't want it at the front of the initial line because it replaces an expression
101+
// that might be in the middle of a line.
102+
if formattedExpansion.hasPrefix(context.initialIndentation.description) {
103+
formattedExpansion = String(formattedExpansion.dropFirst(context.initialIndentation.description.count))
104+
}
105+
expanded = formattedExpansion
85106
} else {
86107
expanded = String(text)
87108
}
@@ -122,13 +143,25 @@ struct ExpandSingleEditorPlaceholder: EditRefactoringProvider {
122143
///
123144
/// Expansion on `closure1` and `normalArg` is the same as `ExpandSingleEditorPlaceholder`.
124145
public struct ExpandEditorPlaceholder: EditRefactoringProvider {
125-
public static func textRefactor(syntax token: TokenSyntax, in context: Void) -> [SourceEdit] {
146+
public struct Context {
147+
public let indentationWidth: Trivia?
148+
149+
public init(indentationWidth: Trivia? = nil) {
150+
self.indentationWidth = indentationWidth
151+
}
152+
}
153+
154+
public static func textRefactor(syntax token: TokenSyntax, in context: Context = Context()) -> [SourceEdit] {
126155
guard let placeholder = token.parent?.as(DeclReferenceExprSyntax.self),
127156
placeholder.baseName.isEditorPlaceholder,
128157
let arg = placeholder.parent?.as(LabeledExprSyntax.self),
129158
let argList = arg.parent?.as(LabeledExprListSyntax.self),
130159
let call = argList.parent?.as(FunctionCallExprSyntax.self),
131-
let expandedTrailingClosures = ExpandEditorPlaceholdersToTrailingClosures.expandTrailingClosurePlaceholders(in: call, ifIncluded: arg)
160+
let expandedTrailingClosures = ExpandEditorPlaceholdersToTrailingClosures.expandTrailingClosurePlaceholders(
161+
in: call,
162+
ifIncluded: arg,
163+
indentationWidth: context.indentationWidth
164+
)
132165
else {
133166
return ExpandSingleEditorPlaceholder.textRefactor(syntax: token)
134167
}
@@ -159,8 +192,16 @@ public struct ExpandEditorPlaceholder: EditRefactoringProvider {
159192
/// }
160193
/// ```
161194
public struct ExpandEditorPlaceholdersToTrailingClosures: SyntaxRefactoringProvider {
162-
public static func refactor(syntax call: FunctionCallExprSyntax, in context: Void = ()) -> FunctionCallExprSyntax? {
163-
return Self.expandTrailingClosurePlaceholders(in: call, ifIncluded: nil)
195+
public struct Context {
196+
public let indentationWidth: Trivia?
197+
198+
public init(indentationWidth: Trivia? = nil) {
199+
self.indentationWidth = indentationWidth
200+
}
201+
}
202+
203+
public static func refactor(syntax call: FunctionCallExprSyntax, in context: Context = Context()) -> FunctionCallExprSyntax? {
204+
return Self.expandTrailingClosurePlaceholders(in: call, ifIncluded: nil, indentationWidth: context.indentationWidth)
164205
}
165206

166207
/// If the given argument is `nil` or one of the last arguments that are all
@@ -170,9 +211,10 @@ public struct ExpandEditorPlaceholdersToTrailingClosures: SyntaxRefactoringProvi
170211
/// Otherwise return nil.
171212
fileprivate static func expandTrailingClosurePlaceholders(
172213
in call: FunctionCallExprSyntax,
173-
ifIncluded arg: LabeledExprSyntax?
214+
ifIncluded arg: LabeledExprSyntax?,
215+
indentationWidth: Trivia?
174216
) -> FunctionCallExprSyntax? {
175-
guard let expanded = call.expandTrailingClosurePlaceholders(ifIncluded: arg) else {
217+
guard let expanded = call.expandTrailingClosurePlaceholders(ifIncluded: arg, indentationWidth: indentationWidth) else {
176218
return nil
177219
}
178220

@@ -260,7 +302,8 @@ extension FunctionCallExprSyntax {
260302
/// closures based on the function types provided by each editor placeholder.
261303
/// Otherwise return nil.
262304
fileprivate func expandTrailingClosurePlaceholders(
263-
ifIncluded: LabeledExprSyntax?
305+
ifIncluded: LabeledExprSyntax?,
306+
indentationWidth: Trivia?
264307
) -> (expr: FunctionCallExprSyntax, numClosures: Int)? {
265308
var includedArg = false
266309
var argsToExpand = 0
@@ -283,9 +326,14 @@ extension FunctionCallExprSyntax {
283326
return nil
284327
}
285328

329+
let lineIndentation = self.firstToken(viewMode: .sourceAccurate)?.indentationOfLine ?? []
330+
286331
var expandedArgs = [LabeledExprSyntax]()
287332
for arg in arguments.suffix(argsToExpand) {
288-
let edits = ExpandSingleEditorPlaceholder.textRefactor(syntax: arg.expression.cast(DeclReferenceExprSyntax.self).baseName)
333+
let edits = ExpandSingleEditorPlaceholder.textRefactor(
334+
syntax: arg.expression.cast(DeclReferenceExprSyntax.self).baseName,
335+
in: ExpandSingleEditorPlaceholder.Context(indentationWidth: indentationWidth, initialIndentation: lineIndentation)
336+
)
289337
guard edits.count == 1, let edit = edits.first, !edit.replacement.isEmpty else {
290338
return nil
291339
}

Tests/SwiftBasicFormatTest/BasicFormatTests.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,4 +610,39 @@ final class BasicFormatTest: XCTestCase {
610610
)
611611
}
612612

613+
func testFormatClosureWithInitialIndentation() throws {
614+
assertFormatted(
615+
tree: ClosureExprSyntax(
616+
statements: CodeBlockItemListSyntax([
617+
CodeBlockItemSyntax(item: CodeBlockItemSyntax.Item(IntegerLiteralExprSyntax(integerLiteral: 2)))
618+
])
619+
),
620+
expected: """
621+
{
622+
2
623+
}
624+
""",
625+
using: BasicFormat(initialIndentation: .spaces(4))
626+
)
627+
}
628+
629+
func testIndentedStandaloneClosureRoundTrips() throws {
630+
let source = """
631+
foo {
632+
"abc"
633+
}
634+
"""
635+
assertFormatted(source: source, expected: source)
636+
}
637+
638+
func testIndentedStandaloneClosureRoundTrips2() throws {
639+
let source = """
640+
foo {
641+
if true {
642+
print("test")
643+
}
644+
}
645+
"""
646+
assertFormatted(source: source, expected: source)
647+
}
613648
}

Tests/SwiftRefactorTest/CallToTrailingClosureTests.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,36 @@ final class CallToTrailingClosuresTest: XCTestCase {
217217

218218
try assertRefactorCall(baseline, expected: expected)
219219
}
220+
221+
func testClosureWithArgumentLabel() throws {
222+
try assertRefactorCall(
223+
"""
224+
foo(arg: 1, closure: { someInt in
225+
226+
})
227+
""",
228+
expected: """
229+
foo(arg: 1) { someInt in
230+
231+
}
232+
"""
233+
)
234+
}
235+
236+
func testCallHasInitialIndentationIndentation() throws {
237+
try assertRefactorCall(
238+
"""
239+
foo(arg: 1, closure: { someInt in
240+
"abc"
241+
})
242+
""",
243+
expected: """
244+
foo(arg: 1) { someInt in
245+
"abc"
246+
}
247+
"""
248+
)
249+
}
220250
}
221251

222252
fileprivate func assertRefactorCall(

0 commit comments

Comments
 (0)