diff --git a/Release Notes/602.md b/Release Notes/602.md index 0d5750bdd2e..7ef16677d8f 100644 --- a/Release Notes/602.md +++ b/Release Notes/602.md @@ -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* diff --git a/Sources/SwiftRefactor/CMakeLists.txt b/Sources/SwiftRefactor/CMakeLists.txt index 1864c71f2ab..8494f3b91cc 100644 --- a/Sources/SwiftRefactor/CMakeLists.txt +++ b/Sources/SwiftRefactor/CMakeLists.txt @@ -8,6 +8,7 @@ add_swift_syntax_library(SwiftRefactor AddSeparatorsToIntegerLiteral.swift + CallLikeSyntax.swift CallToTrailingClosures.swift ConvertComputedPropertyToStored.swift ConvertComputedPropertyToZeroParameterFunction.swift diff --git a/Sources/SwiftRefactor/CallLikeSyntax.swift b/Sources/SwiftRefactor/CallLikeSyntax.swift new file mode 100644 index 00000000000..c620a52fe97 --- /dev/null +++ b/Sources/SwiftRefactor/CallLikeSyntax.swift @@ -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 + } +} diff --git a/Sources/SwiftRefactor/CallToTrailingClosures.swift b/Sources/SwiftRefactor/CallToTrailingClosures.swift index f2781d4b7ed..858c1185b61 100644 --- a/Sources/SwiftRefactor/CallToTrailingClosures.swift +++ b/Sources/SwiftRefactor/CallToTrailingClosures.swift @@ -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( + 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 diff --git a/Sources/SwiftRefactor/ExpandEditorPlaceholder.swift b/Sources/SwiftRefactor/ExpandEditorPlaceholder.swift index 7bf87269489..50854f65e37 100644 --- a/Sources/SwiftRefactor/ExpandEditorPlaceholder.swift +++ b/Sources/SwiftRefactor/ExpandEditorPlaceholder.swift @@ -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, @@ -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() @@ -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( + in call: C, ifIncluded arg: LabeledExprSyntax?, context: Context - ) -> FunctionCallExprSyntax? { + ) -> C? { switch context.format { case let .custom(formatter, allowNestedPlaceholders: allowNesting): let expanded = call.expandClosurePlaceholders( @@ -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) } } } @@ -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 @@ -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() { diff --git a/Tests/SwiftRefactorTest/ExpandEditorPlaceholderTests.swift b/Tests/SwiftRefactorTest/ExpandEditorPlaceholderTests.swift index 859963a737c..97d5d1660b2 100644 --- a/Tests/SwiftRefactorTest/ExpandEditorPlaceholderTests.swift +++ b/Tests/SwiftRefactorTest/ExpandEditorPlaceholderTests.swift @@ -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( @@ -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 @@ -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 @@ -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), @@ -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)