Skip to content

Commit 3a79e39

Browse files
hamishknightthunderseethe
authored andcommitted
Handle macro expansion args in placeholder expansion
Update `ExpandEditorPlaceholdersToLiteralClosures` and `CallToTrailingClosures` such that they can handle macro expansion exprs and decls. This unfortunately requires changing them such that they take a `Syntax` input and output to satisfy the conformance to `SyntaxRefactoringProvider`, but that seems preferable to making the refactoring types themselves generic. If in the future we decide to expose `CallLikeSyntax` as a public protocol, we could decide to expose additional generic `refactor` overloads here. However it's not clear there's enough clients of these APIs to motivate doing that in this PR.
1 parent 62b790b commit 3a79e39

File tree

6 files changed

+186
-21
lines changed

6 files changed

+186
-21
lines changed

Release Notes/602.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,13 @@
5454
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/3028
5555
- Migration steps: Use `AttributeSyntax.Arguments.argumentList(LabeledExprListSyntax)` instead.
5656
- Notes: Removed cases from `AttributeSyntax.Arguments`: `token(TokenSyntax)`, `string(StringLiteralExprSyntax)`, `conventionArguments(ConventionAttributeArgumentsSyntax)`, `conventionWitnessMethodArguments(ConventionWitnessMethodAttributeArgumentsSyntax)`, `opaqueReturnTypeOfAttributeArguments(OpaqueReturnTypeOfAttributeArgumentsSyntax)`, `exposeAttributeArguments(ExposeAttributeArgumentsSyntax)`, `underscorePrivateAttributeArguments(UnderscorePrivateAttributeArgumentsSyntax)`, and `unavailableFromAsyncArguments(UnavailableFromAsyncAttributeArgumentsSyntax)`. Removed Syntax kinds: `ConventionAttributeArgumentsSyntax`, `ConventionWitnessMethodAttributeArgumentsSyntax`, `OpaqueReturnTypeOfAttributeArgumentsSyntax`, `ExposeAttributeArgumentsSyntax`, `UnderscorePrivateAttributeArgumentsSyntax`, and `UnavailableFromAsyncAttributeArgumentsSyntax`.
57-
,
57+
58+
- `ExpandEditorPlaceholdersToLiteralClosures` & `CallToTrailingClosures` now take a `Syntax` parameter
59+
- Description: These refactorings now take an arbitrary `Syntax` and return a `Syntax?`. If a non-function-like syntax node is passed, `nil` is returned. The previous `FunctionCallExprSyntax` overloads are deprecated.
60+
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/3092
61+
- Migration steps: Insert a `Syntax(...)` initializer call for the argument, and cast the result with `.as(...)` if necessary.
62+
- Notes: This allows the refactorings to correctly handle macro expansion expressions and declarations.
63+
5864
## Template
5965

6066
- *Affected API or two word description*

Sources/SwiftRefactor/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
add_swift_syntax_library(SwiftRefactor
1010
AddSeparatorsToIntegerLiteral.swift
11+
CallLikeSyntax.swift
1112
CallToTrailingClosures.swift
1213
ConvertComputedPropertyToStored.swift
1314
ConvertComputedPropertyToZeroParameterFunction.swift
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 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+
#if compiler(>=6)
14+
public import SwiftSyntax
15+
#else
16+
import SwiftSyntax
17+
#endif
18+
19+
// TODO: We ought to consider exposing this as a public syntax protocol.
20+
@_spi(Testing) public protocol CallLikeSyntax: SyntaxProtocol {
21+
var arguments: LabeledExprListSyntax { get set }
22+
var leftParen: TokenSyntax? { get set }
23+
var rightParen: TokenSyntax? { get set }
24+
var trailingClosure: ClosureExprSyntax? { get set }
25+
var additionalTrailingClosures: MultipleTrailingClosureElementListSyntax { get set }
26+
}
27+
@_spi(Testing) extension FunctionCallExprSyntax: CallLikeSyntax {}
28+
@_spi(Testing) extension MacroExpansionExprSyntax: CallLikeSyntax {}
29+
@_spi(Testing) extension MacroExpansionDeclSyntax: CallLikeSyntax {}
30+
31+
extension SyntaxProtocol {
32+
@_spi(Testing) public func asProtocol(_: CallLikeSyntax.Protocol) -> (any CallLikeSyntax)? {
33+
Syntax(self).asProtocol(SyntaxProtocol.self) as? CallLikeSyntax
34+
}
35+
}

Sources/SwiftRefactor/CallToTrailingClosures.swift

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,41 @@ public struct CallToTrailingClosures: SyntaxRefactoringProvider {
5050
}
5151
}
5252

53+
public typealias Input = Syntax
54+
public typealias Output = Syntax
55+
56+
/// Apply the refactoring to a given syntax node. If either a
57+
/// non-function-like syntax node is passed, or the refactoring fails,
58+
/// `nil` is returned.
5359
// TODO: Rather than returning nil, we should consider throwing errors with
5460
// appropriate messages instead.
61+
public static func refactor(
62+
syntax: Syntax,
63+
in context: Context = Context()
64+
) -> Syntax? {
65+
guard let call = syntax.asProtocol(CallLikeSyntax.self) else { return nil }
66+
return Syntax(fromProtocol: _refactor(syntax: call, in: context))
67+
}
68+
69+
@available(*, deprecated, message: "Pass a Syntax argument instead of FunctionCallExprSyntax")
5570
public static func refactor(
5671
syntax call: FunctionCallExprSyntax,
5772
in context: Context = Context()
5873
) -> FunctionCallExprSyntax? {
74+
_refactor(syntax: call, in: context)
75+
}
76+
77+
internal static func _refactor<C: CallLikeSyntax>(
78+
syntax call: C,
79+
in context: Context = Context()
80+
) -> C? {
5981
let converted = call.convertToTrailingClosures(from: context.startAtArgument)
60-
return converted?.formatted().as(FunctionCallExprSyntax.self)
82+
return converted?.formatted().as(C.self)
6183
}
6284
}
6385

64-
extension FunctionCallExprSyntax {
65-
fileprivate func convertToTrailingClosures(from startAtArgument: Int) -> FunctionCallExprSyntax? {
86+
extension CallLikeSyntax {
87+
fileprivate func convertToTrailingClosures(from startAtArgument: Int) -> Self? {
6688
guard trailingClosure == nil, additionalTrailingClosures.isEmpty, leftParen != nil, rightParen != nil else {
6789
// Already have trailing closures
6890
return nil

Sources/SwiftRefactor/ExpandEditorPlaceholder.swift

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ public struct ExpandEditorPlaceholder: EditRefactoringProvider {
180180
placeholder.baseName.isEditorPlaceholder,
181181
let arg = placeholder.parent?.as(LabeledExprSyntax.self),
182182
let argList = arg.parent?.as(LabeledExprListSyntax.self),
183-
let call = argList.parent?.as(FunctionCallExprSyntax.self),
183+
let call = argList.parent?.asProtocol(CallLikeSyntax.self),
184184
let expandedClosures = ExpandEditorPlaceholdersToLiteralClosures.expandClosurePlaceholders(
185185
in: call,
186186
ifIncluded: arg,
@@ -266,6 +266,26 @@ public struct ExpandEditorPlaceholdersToLiteralClosures: SyntaxRefactoringProvid
266266
}
267267
}
268268

269+
public typealias Input = Syntax
270+
public typealias Output = Syntax
271+
272+
/// Apply the refactoring to a given syntax node. If either a
273+
/// non-function-like syntax node is passed, or the refactoring fails,
274+
/// `nil` is returned.
275+
public static func refactor(
276+
syntax: Syntax,
277+
in context: Context = Context()
278+
) -> Syntax? {
279+
guard let call = syntax.asProtocol(CallLikeSyntax.self) else { return nil }
280+
let expanded = Self.expandClosurePlaceholders(
281+
in: call,
282+
ifIncluded: nil,
283+
context: context
284+
)
285+
return Syntax(fromProtocol: expanded)
286+
}
287+
288+
@available(*, deprecated, message: "Pass a Syntax argument instead of FunctionCallExprSyntax")
269289
public static func refactor(
270290
syntax call: FunctionCallExprSyntax,
271291
in context: Context = Context()
@@ -282,11 +302,11 @@ public struct ExpandEditorPlaceholdersToLiteralClosures: SyntaxRefactoringProvid
282302
/// closure, then return a replacement of this call with one that uses
283303
/// closures based on the function types provided by each editor placeholder.
284304
/// Otherwise return nil.
285-
fileprivate static func expandClosurePlaceholders(
286-
in call: FunctionCallExprSyntax,
305+
fileprivate static func expandClosurePlaceholders<C: CallLikeSyntax>(
306+
in call: C,
287307
ifIncluded arg: LabeledExprSyntax?,
288308
context: Context
289-
) -> FunctionCallExprSyntax? {
309+
) -> C? {
290310
switch context.format {
291311
case let .custom(formatter, allowNestedPlaceholders: allowNesting):
292312
let expanded = call.expandClosurePlaceholders(
@@ -305,11 +325,7 @@ public struct ExpandEditorPlaceholdersToLiteralClosures: SyntaxRefactoringProvid
305325
let callToTrailingContext = CallToTrailingClosures.Context(
306326
startAtArgument: call.arguments.count - expanded.numClosures
307327
)
308-
guard let trailing = CallToTrailingClosures.refactor(syntax: expanded.expr, in: callToTrailingContext) else {
309-
return nil
310-
}
311-
312-
return trailing
328+
return CallToTrailingClosures._refactor(syntax: expanded.expr, in: callToTrailingContext)
313329
}
314330
}
315331
}
@@ -382,7 +398,7 @@ extension TupleTypeElementSyntax {
382398
}
383399
}
384400

385-
extension FunctionCallExprSyntax {
401+
extension CallLikeSyntax {
386402
/// If the given argument is `nil` or one of the last arguments that are all
387403
/// function-typed placeholders and this call doesn't have a trailing
388404
/// closure, then return a replacement of this call with one that uses
@@ -393,7 +409,7 @@ extension FunctionCallExprSyntax {
393409
indentationWidth: Trivia? = nil,
394410
customFormat: BasicFormat? = nil,
395411
allowNestedPlaceholders: Bool = false
396-
) -> (expr: FunctionCallExprSyntax, numClosures: Int)? {
412+
) -> (expr: Self, numClosures: Int)? {
397413
var includedArg = false
398414
var argsToExpand = 0
399415
for arg in arguments.reversed() {

Tests/SwiftRefactorTest/ExpandEditorPlaceholderTests.swift

Lines changed: 91 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,58 @@ final class ExpandEditorPlaceholderTests: XCTestCase {
451451
format: .testCustom()
452452
)
453453
}
454+
455+
func testMacroTrailingClosureExpansion1() throws {
456+
try assertRefactorPlaceholderToken(
457+
"#foo(\(closurePlaceholder), \(intPlaceholder))",
458+
expected: """
459+
{
460+
\(voidPlaceholder)
461+
}
462+
"""
463+
)
464+
}
465+
466+
func testMacroTrailingClosureExpansion2() throws {
467+
let call = "#foo(fn: \(closureWithArgPlaceholder))"
468+
let expanded = """
469+
#foo { someInt in
470+
\(stringPlaceholder)
471+
}
472+
"""
473+
474+
try assertRefactorPlaceholderCall(call, expected: expanded)
475+
try assertExpandEditorPlaceholdersToClosures(call, expected: expanded)
476+
}
477+
478+
func testMacroTrailingClosureExpansion3() throws {
479+
let call = "#foo(fn1: \(closurePlaceholder), fn2: \(closureWithArgPlaceholder))"
480+
let expanded = """
481+
#foo {
482+
\(voidPlaceholder)
483+
} fn2: { someInt in
484+
\(stringPlaceholder)
485+
}
486+
"""
487+
488+
try assertRefactorPlaceholderCall(call, expected: expanded)
489+
try assertExpandEditorPlaceholdersToClosures(call, expected: expanded)
490+
}
491+
492+
func testMacroTrailingClosureExpansion4() throws {
493+
try assertExpandEditorPlaceholdersToClosures(
494+
decl: """
495+
#foo(fn1: \(closurePlaceholder), fn2: \(closurePlaceholder))
496+
""",
497+
expected: """
498+
#foo {
499+
\(voidPlaceholder)
500+
} fn2: {
501+
\(voidPlaceholder)
502+
}
503+
"""
504+
)
505+
}
454506
}
455507

456508
fileprivate func assertRefactorPlaceholder(
@@ -489,7 +541,7 @@ fileprivate func assertRefactorPlaceholderCall(
489541
line: UInt = #line
490542
) throws {
491543
var parser = Parser(expr)
492-
let call = try XCTUnwrap(ExprSyntax.parse(from: &parser).as(FunctionCallExprSyntax.self), file: file, line: line)
544+
let call = try XCTUnwrap(ExprSyntax.parse(from: &parser).asProtocol(CallLikeSyntax.self), file: file, line: line)
493545
let arg = call.arguments[call.arguments.index(at: placeholder)]
494546
let token: TokenSyntax = try XCTUnwrap(arg.expression.as(DeclReferenceExprSyntax.self), file: file, line: line)
495547
.baseName
@@ -513,7 +565,7 @@ fileprivate func assertRefactorPlaceholderToken(
513565
line: UInt = #line
514566
) throws {
515567
var parser = Parser(expr)
516-
let call = try XCTUnwrap(ExprSyntax.parse(from: &parser).as(FunctionCallExprSyntax.self), file: file, line: line)
568+
let call = try XCTUnwrap(ExprSyntax.parse(from: &parser).asProtocol(CallLikeSyntax.self), file: file, line: line)
517569
let arg = call.arguments[call.arguments.index(at: placeholder)]
518570
let token: TokenSyntax = try XCTUnwrap(arg.expression.as(DeclReferenceExprSyntax.self), file: file, line: line)
519571
.baseName
@@ -529,15 +581,12 @@ fileprivate func assertRefactorPlaceholderToken(
529581
}
530582

531583
fileprivate func assertExpandEditorPlaceholdersToClosures(
532-
_ expr: String,
584+
_ call: some CallLikeSyntax,
533585
expected: String,
534586
format: ExpandEditorPlaceholdersToLiteralClosures.Context.Format = .trailing(indentationWidth: nil),
535587
file: StaticString = #filePath,
536588
line: UInt = #line
537589
) throws {
538-
var parser = Parser(expr)
539-
let call = try XCTUnwrap(ExprSyntax.parse(from: &parser).as(FunctionCallExprSyntax.self), file: file, line: line)
540-
541590
try assertRefactor(
542591
call,
543592
context: ExpandEditorPlaceholdersToLiteralClosures.Context(format: format),
@@ -548,6 +597,42 @@ fileprivate func assertExpandEditorPlaceholdersToClosures(
548597
)
549598
}
550599

600+
fileprivate func assertExpandEditorPlaceholdersToClosures(
601+
_ expr: String,
602+
expected: String,
603+
format: ExpandEditorPlaceholdersToLiteralClosures.Context.Format = .trailing(indentationWidth: nil),
604+
file: StaticString = #filePath,
605+
line: UInt = #line
606+
) throws {
607+
var parser = Parser(expr)
608+
let call = try XCTUnwrap(ExprSyntax.parse(from: &parser).asProtocol(CallLikeSyntax.self), file: file, line: line)
609+
try assertExpandEditorPlaceholdersToClosures(
610+
call,
611+
expected: expected,
612+
format: format,
613+
file: file,
614+
line: line
615+
)
616+
}
617+
618+
fileprivate func assertExpandEditorPlaceholdersToClosures(
619+
decl: String,
620+
expected: String,
621+
format: ExpandEditorPlaceholdersToLiteralClosures.Context.Format = .trailing(indentationWidth: nil),
622+
file: StaticString = #filePath,
623+
line: UInt = #line
624+
) throws {
625+
var parser = Parser(decl)
626+
let call = try XCTUnwrap(DeclSyntax.parse(from: &parser).asProtocol(CallLikeSyntax.self), file: file, line: line)
627+
try assertExpandEditorPlaceholdersToClosures(
628+
call,
629+
expected: expected,
630+
format: format,
631+
file: file,
632+
line: line
633+
)
634+
}
635+
551636
fileprivate extension ExpandEditorPlaceholdersToLiteralClosures.Context.Format {
552637
static func testCustom(indentationWidth: Trivia? = nil) -> Self {
553638
.custom(CustomClosureFormat(indentationWidth: indentationWidth), allowNestedPlaceholders: true)

0 commit comments

Comments
 (0)