Skip to content

Handle macro expansion args in placeholder expansion #3092

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Release Notes/602.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,13 @@
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/3028
- Migration steps: Use `AttributeSyntax.Arguments.argumentList(LabeledExprListSyntax)` instead.
- 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`.
,

- `ExpandEditorPlaceholdersToLiteralClosures` & `CallToTrailingClosures` now take a `Syntax` parameter
- 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.
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/3092
- Migration steps: Insert a `Syntax(...)` initializer call for the argument, and cast the result with `.as(...)` if necessary.
- Notes: This allows the refactorings to correctly handle macro expansion expressions and declarations.

## Template

- *Affected API or two word description*
Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftRefactor/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

add_swift_syntax_library(SwiftRefactor
AddSeparatorsToIntegerLiteral.swift
CallLikeSyntax.swift
CallToTrailingClosures.swift
ConvertComputedPropertyToStored.swift
ConvertComputedPropertyToZeroParameterFunction.swift
Expand Down
35 changes: 35 additions & 0 deletions Sources/SwiftRefactor/CallLikeSyntax.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

#if compiler(>=6)
public import SwiftSyntax
#else
import SwiftSyntax
#endif

// TODO: We ought to consider exposing this as a public syntax protocol.
@_spi(Testing) public protocol CallLikeSyntax: SyntaxProtocol {
var arguments: LabeledExprListSyntax { get set }
var leftParen: TokenSyntax? { get set }
var rightParen: TokenSyntax? { get set }
var trailingClosure: ClosureExprSyntax? { get set }
var additionalTrailingClosures: MultipleTrailingClosureElementListSyntax { get set }
}
@_spi(Testing) extension FunctionCallExprSyntax: CallLikeSyntax {}
@_spi(Testing) extension MacroExpansionExprSyntax: CallLikeSyntax {}
@_spi(Testing) extension MacroExpansionDeclSyntax: CallLikeSyntax {}

extension SyntaxProtocol {
@_spi(Testing) public func asProtocol(_: CallLikeSyntax.Protocol) -> (any CallLikeSyntax)? {
Syntax(self).asProtocol(SyntaxProtocol.self) as? CallLikeSyntax
}
}
28 changes: 25 additions & 3 deletions Sources/SwiftRefactor/CallToTrailingClosures.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,41 @@ public struct CallToTrailingClosures: SyntaxRefactoringProvider {
}
}

public typealias Input = Syntax
public typealias Output = Syntax

/// Apply the refactoring to a given syntax node. If either a
/// non-function-like syntax node is passed, or the refactoring fails,
/// `nil` is returned.
// TODO: Rather than returning nil, we should consider throwing errors with
// appropriate messages instead.
public static func refactor(
syntax: Syntax,
in context: Context = Context()
) -> Syntax? {
guard let call = syntax.asProtocol(CallLikeSyntax.self) else { return nil }
return Syntax(fromProtocol: _refactor(syntax: call, in: context))
}

@available(*, deprecated, message: "Pass a Syntax argument instead of FunctionCallExprSyntax")
public static func refactor(
syntax call: FunctionCallExprSyntax,
in context: Context = Context()
) -> FunctionCallExprSyntax? {
_refactor(syntax: call, in: context)
}

internal static func _refactor<C: CallLikeSyntax>(
syntax call: C,
in context: Context = Context()
) -> C? {
let converted = call.convertToTrailingClosures(from: context.startAtArgument)
return converted?.formatted().as(FunctionCallExprSyntax.self)
return converted?.formatted().as(C.self)
}
}

extension FunctionCallExprSyntax {
fileprivate func convertToTrailingClosures(from startAtArgument: Int) -> FunctionCallExprSyntax? {
extension CallLikeSyntax {
fileprivate func convertToTrailingClosures(from startAtArgument: Int) -> Self? {
guard trailingClosure == nil, additionalTrailingClosures.isEmpty, leftParen != nil, rightParen != nil else {
// Already have trailing closures
return nil
Expand Down
38 changes: 27 additions & 11 deletions Sources/SwiftRefactor/ExpandEditorPlaceholder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ public struct ExpandEditorPlaceholder: EditRefactoringProvider {
placeholder.baseName.isEditorPlaceholder,
let arg = placeholder.parent?.as(LabeledExprSyntax.self),
let argList = arg.parent?.as(LabeledExprListSyntax.self),
let call = argList.parent?.as(FunctionCallExprSyntax.self),
let call = argList.parent?.asProtocol(CallLikeSyntax.self),
let expandedClosures = ExpandEditorPlaceholdersToLiteralClosures.expandClosurePlaceholders(
in: call,
ifIncluded: arg,
Expand Down Expand Up @@ -266,6 +266,26 @@ public struct ExpandEditorPlaceholdersToLiteralClosures: SyntaxRefactoringProvid
}
}

public typealias Input = Syntax
public typealias Output = Syntax

/// Apply the refactoring to a given syntax node. If either a
/// non-function-like syntax node is passed, or the refactoring fails,
/// `nil` is returned.
public static func refactor(
syntax: Syntax,
in context: Context = Context()
) -> Syntax? {
guard let call = syntax.asProtocol(CallLikeSyntax.self) else { return nil }
let expanded = Self.expandClosurePlaceholders(
in: call,
ifIncluded: nil,
context: context
)
return Syntax(fromProtocol: expanded)
}

@available(*, deprecated, message: "Pass a Syntax argument instead of FunctionCallExprSyntax")
public static func refactor(
syntax call: FunctionCallExprSyntax,
in context: Context = Context()
Expand All @@ -282,11 +302,11 @@ public struct ExpandEditorPlaceholdersToLiteralClosures: SyntaxRefactoringProvid
/// closure, then return a replacement of this call with one that uses
/// closures based on the function types provided by each editor placeholder.
/// Otherwise return nil.
fileprivate static func expandClosurePlaceholders(
in call: FunctionCallExprSyntax,
fileprivate static func expandClosurePlaceholders<C: CallLikeSyntax>(
in call: C,
ifIncluded arg: LabeledExprSyntax?,
context: Context
) -> FunctionCallExprSyntax? {
) -> C? {
switch context.format {
case let .custom(formatter, allowNestedPlaceholders: allowNesting):
let expanded = call.expandClosurePlaceholders(
Expand All @@ -305,11 +325,7 @@ public struct ExpandEditorPlaceholdersToLiteralClosures: SyntaxRefactoringProvid
let callToTrailingContext = CallToTrailingClosures.Context(
startAtArgument: call.arguments.count - expanded.numClosures
)
guard let trailing = CallToTrailingClosures.refactor(syntax: expanded.expr, in: callToTrailingContext) else {
return nil
}

return trailing
return CallToTrailingClosures._refactor(syntax: expanded.expr, in: callToTrailingContext)
}
}
}
Expand Down Expand Up @@ -382,7 +398,7 @@ extension TupleTypeElementSyntax {
}
}

extension FunctionCallExprSyntax {
extension CallLikeSyntax {
/// If the given argument is `nil` or one of the last arguments that are all
/// function-typed placeholders and this call doesn't have a trailing
/// closure, then return a replacement of this call with one that uses
Expand All @@ -393,7 +409,7 @@ extension FunctionCallExprSyntax {
indentationWidth: Trivia? = nil,
customFormat: BasicFormat? = nil,
allowNestedPlaceholders: Bool = false
) -> (expr: FunctionCallExprSyntax, numClosures: Int)? {
) -> (expr: Self, numClosures: Int)? {
var includedArg = false
var argsToExpand = 0
for arg in arguments.reversed() {
Expand Down
97 changes: 91 additions & 6 deletions Tests/SwiftRefactorTest/ExpandEditorPlaceholderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,58 @@ final class ExpandEditorPlaceholderTests: XCTestCase {
format: .testCustom()
)
}

func testMacroTrailingClosureExpansion1() throws {
try assertRefactorPlaceholderToken(
"#foo(\(closurePlaceholder), \(intPlaceholder))",
expected: """
{
\(voidPlaceholder)
}
"""
)
}

func testMacroTrailingClosureExpansion2() throws {
let call = "#foo(fn: \(closureWithArgPlaceholder))"
let expanded = """
#foo { someInt in
\(stringPlaceholder)
}
"""

try assertRefactorPlaceholderCall(call, expected: expanded)
try assertExpandEditorPlaceholdersToClosures(call, expected: expanded)
}

func testMacroTrailingClosureExpansion3() throws {
let call = "#foo(fn1: \(closurePlaceholder), fn2: \(closureWithArgPlaceholder))"
let expanded = """
#foo {
\(voidPlaceholder)
} fn2: { someInt in
\(stringPlaceholder)
}
"""

try assertRefactorPlaceholderCall(call, expected: expanded)
try assertExpandEditorPlaceholdersToClosures(call, expected: expanded)
}

func testMacroTrailingClosureExpansion4() throws {
try assertExpandEditorPlaceholdersToClosures(
decl: """
#foo(fn1: \(closurePlaceholder), fn2: \(closurePlaceholder))
""",
expected: """
#foo {
\(voidPlaceholder)
} fn2: {
\(voidPlaceholder)
}
"""
)
}
}

fileprivate func assertRefactorPlaceholder(
Expand Down Expand Up @@ -489,7 +541,7 @@ fileprivate func assertRefactorPlaceholderCall(
line: UInt = #line
) throws {
var parser = Parser(expr)
let call = try XCTUnwrap(ExprSyntax.parse(from: &parser).as(FunctionCallExprSyntax.self), file: file, line: line)
let call = try XCTUnwrap(ExprSyntax.parse(from: &parser).asProtocol(CallLikeSyntax.self), file: file, line: line)
let arg = call.arguments[call.arguments.index(at: placeholder)]
let token: TokenSyntax = try XCTUnwrap(arg.expression.as(DeclReferenceExprSyntax.self), file: file, line: line)
.baseName
Expand All @@ -513,7 +565,7 @@ fileprivate func assertRefactorPlaceholderToken(
line: UInt = #line
) throws {
var parser = Parser(expr)
let call = try XCTUnwrap(ExprSyntax.parse(from: &parser).as(FunctionCallExprSyntax.self), file: file, line: line)
let call = try XCTUnwrap(ExprSyntax.parse(from: &parser).asProtocol(CallLikeSyntax.self), file: file, line: line)
let arg = call.arguments[call.arguments.index(at: placeholder)]
let token: TokenSyntax = try XCTUnwrap(arg.expression.as(DeclReferenceExprSyntax.self), file: file, line: line)
.baseName
Expand All @@ -529,15 +581,12 @@ fileprivate func assertRefactorPlaceholderToken(
}

fileprivate func assertExpandEditorPlaceholdersToClosures(
_ expr: String,
_ call: some CallLikeSyntax,
expected: String,
format: ExpandEditorPlaceholdersToLiteralClosures.Context.Format = .trailing(indentationWidth: nil),
file: StaticString = #filePath,
line: UInt = #line
) throws {
var parser = Parser(expr)
let call = try XCTUnwrap(ExprSyntax.parse(from: &parser).as(FunctionCallExprSyntax.self), file: file, line: line)

try assertRefactor(
call,
context: ExpandEditorPlaceholdersToLiteralClosures.Context(format: format),
Expand All @@ -548,6 +597,42 @@ fileprivate func assertExpandEditorPlaceholdersToClosures(
)
}

fileprivate func assertExpandEditorPlaceholdersToClosures(
_ expr: String,
expected: String,
format: ExpandEditorPlaceholdersToLiteralClosures.Context.Format = .trailing(indentationWidth: nil),
file: StaticString = #filePath,
line: UInt = #line
) throws {
var parser = Parser(expr)
let call = try XCTUnwrap(ExprSyntax.parse(from: &parser).asProtocol(CallLikeSyntax.self), file: file, line: line)
try assertExpandEditorPlaceholdersToClosures(
call,
expected: expected,
format: format,
file: file,
line: line
)
}

fileprivate func assertExpandEditorPlaceholdersToClosures(
decl: String,
expected: String,
format: ExpandEditorPlaceholdersToLiteralClosures.Context.Format = .trailing(indentationWidth: nil),
file: StaticString = #filePath,
line: UInt = #line
) throws {
var parser = Parser(decl)
let call = try XCTUnwrap(DeclSyntax.parse(from: &parser).asProtocol(CallLikeSyntax.self), file: file, line: line)
try assertExpandEditorPlaceholdersToClosures(
call,
expected: expected,
format: format,
file: file,
line: line
)
}

fileprivate extension ExpandEditorPlaceholdersToLiteralClosures.Context.Format {
static func testCustom(indentationWidth: Trivia? = nil) -> Self {
.custom(CustomClosureFormat(indentationWidth: indentationWidth), allowNestedPlaceholders: true)
Expand Down
Loading