diff --git a/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt b/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt index 5a838463580..657126f5034 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt +++ b/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt @@ -16,6 +16,7 @@ add_swift_host_library(SwiftCompilerPluginMessageHandling target_link_libraries(SwiftCompilerPluginMessageHandling PUBLIC SwiftSyntax + SwiftBasicFormat SwiftDiagnostics SwiftParser SwiftSyntaxMacros diff --git a/Sources/SwiftCompilerPluginMessageHandling/Macros.swift b/Sources/SwiftCompilerPluginMessageHandling/Macros.swift index aa31ed0907c..4675d0ea019 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/Macros.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/Macros.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------===// +import SwiftBasicFormat import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxMacros @@ -49,21 +50,21 @@ extension CompilerPluginMessageHandler { try exprMacroDef.expansion(of: node, in: context) } let rewritten = try _openExistential(macroSyntax, do: _expand) - expandedSource = rewritten.description + expandedSource = rewritten.formattedExpansion(macroDefinition.formatMode) case let declMacroDef as DeclarationMacro.Type: func _expand(node: Node) throws -> [DeclSyntax] { try declMacroDef.expansion(of: node, in: context) } let rewritten = try _openExistential(macroSyntax, do: _expand) - expandedSource = CodeBlockItemListSyntax(rewritten.map { CodeBlockItemSyntax(item: .decl($0)) }).description + expandedSource = CodeBlockItemListSyntax(rewritten.map { CodeBlockItemSyntax(item: .decl($0)) }).formattedExpansion(macroDefinition.formatMode) case let codeItemMacroDef as CodeItemMacro.Type: func _expand(node: Node) throws -> [CodeBlockItemSyntax] { try codeItemMacroDef.expansion(of: node, in: context) } let rewritten = try _openExistential(macroSyntax, do: _expand) - expandedSource = CodeBlockItemListSyntax(rewritten).description + expandedSource = CodeBlockItemListSyntax(rewritten).formattedExpansion(macroDefinition.formatMode) default: throw MacroExpansionError.unmathedMacroRole @@ -113,7 +114,7 @@ extension CompilerPluginMessageHandler { in: context ) expandedSources = accessors.map { - $0.trimmedDescription + $0.formattedExpansion(macroDefinition.formatMode) } case (let attachedMacro as MemberAttributeMacro.Type, .memberAttribute): @@ -145,7 +146,7 @@ extension CompilerPluginMessageHandler { // Form a buffer containing an attribute list to return to the caller. expandedSources = attributes.map { - $0.trimmedDescription + $0.formattedExpansion(macroDefinition.formatMode) } case (let attachedMacro as MemberMacro.Type, .member): @@ -170,7 +171,7 @@ extension CompilerPluginMessageHandler { let members = try _openExistential(declGroup, do: expandMemberMacro) // Form a buffer of member declarations to return to the caller. - expandedSources = members.map { $0.trimmedDescription } + expandedSources = members.map { $0.formattedExpansion(macroDefinition.formatMode) } case (let attachedMacro as PeerMacro.Type, .peer): let peers = try attachedMacro.expansion( @@ -181,7 +182,7 @@ extension CompilerPluginMessageHandler { // Form a buffer of peer declarations to return to the caller. expandedSources = peers.map { - $0.trimmedDescription + $0.formattedExpansion(macroDefinition.formatMode) } case (let attachedMacro as ConformanceMacro.Type, .conformance): @@ -234,3 +235,18 @@ extension CompilerPluginMessageHandler { ) } } + +fileprivate extension SyntaxProtocol { + /// Perform a format if required and then trim any leading/trailing + /// whitespace. + func formattedExpansion(_ mode: FormatMode) -> String { + let formatted: Syntax + switch mode { + case .auto: + formatted = self.formatted() + case .disabled: + formatted = Syntax(self) + } + return formatted.trimmedDescription(matching: { $0.isWhitespace }) + } +} diff --git a/Sources/SwiftSyntax/Syntax.swift b/Sources/SwiftSyntax/Syntax.swift index b9af71fe0b8..9c202968a56 100644 --- a/Sources/SwiftSyntax/Syntax.swift +++ b/Sources/SwiftSyntax/Syntax.swift @@ -541,13 +541,35 @@ public extension SyntaxProtocol { return self.with(\.leadingTrivia, []).with(\.trailingTrivia, []) } - /// The description of this node without the leading trivia of the first token - /// in the node and the trailing trivia of the last token in the node. + /// A copy of this node with pieces that match `matching` trimmed from the + /// leading trivia of the first token and trailing trivia of the last token. + func trimmed(matching filter: (TriviaPiece) -> Bool) -> Self { + // TODO: Should only need one new node here + return self.with( + \.leadingTrivia, + Trivia(pieces: leadingTrivia.pieces.drop(while: filter)) + ).with( + \.trailingTrivia, + Trivia(pieces: trailingTrivia.pieces.reversed().drop(while: filter).reversed()) + ) + } + + /// The description of this node with leading whitespace of the first token + /// and trailing whitespace of the last token removed. var trimmedDescription: String { - // TODO: We shouldn't need to create to copies just to get the description - // without trivia. + // TODO: We shouldn't need to create to copies just to get the trimmed + // description. return self.trimmed.description } + + /// The description of this node with pieces that match `matching` removed + /// from the leading trivia of the first token and trailing trivia of the + /// last token. + func trimmedDescription(matching filter: (TriviaPiece) -> Bool) -> String { + // TODO: We shouldn't need to create to copies just to get the trimmed + // description. + return self.trimmed(matching: filter).description + } } /// Provides debug descriptions for a node diff --git a/Sources/SwiftSyntaxMacros/CMakeLists.txt b/Sources/SwiftSyntaxMacros/CMakeLists.txt index f95e55fa732..4affa489a10 100644 --- a/Sources/SwiftSyntaxMacros/CMakeLists.txt +++ b/Sources/SwiftSyntaxMacros/CMakeLists.txt @@ -15,6 +15,7 @@ add_swift_host_library(SwiftSyntaxMacros MacroProtocols/ExpressionMacro.swift MacroProtocols/FreestandingMacro.swift MacroProtocols/Macro.swift + MacroProtocols/Macro+Format.swift MacroProtocols/MemberAttributeMacro.swift MacroProtocols/MemberMacro.swift MacroProtocols/PeerMacro.swift diff --git a/Sources/SwiftSyntaxMacros/MacroProtocols/Macro+Format.swift b/Sources/SwiftSyntaxMacros/MacroProtocols/Macro+Format.swift new file mode 100644 index 00000000000..0672ca8877d --- /dev/null +++ b/Sources/SwiftSyntaxMacros/MacroProtocols/Macro+Format.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 +// +//===----------------------------------------------------------------------===// + +/// Describes the mode to use to format the result of an expansion. +public enum FormatMode { + /// Perform a basic format of the expansion. This is primarily for inserting + /// whitespace as required (eg. between two keywords), but also adds simple + /// newline and indentation. + case auto + + /// Disable automatically formatting the expanded macro. Trivia must be + /// manually inserted where required (eg. adding spaces between keywords). + case disabled +} + +public extension Macro { + static var formatMode: FormatMode { + return .auto + } +} diff --git a/Sources/SwiftSyntaxMacros/MacroProtocols/Macro.swift b/Sources/SwiftSyntaxMacros/MacroProtocols/Macro.swift index 90f87267a5d..6203a6f9127 100644 --- a/Sources/SwiftSyntaxMacros/MacroProtocols/Macro.swift +++ b/Sources/SwiftSyntaxMacros/MacroProtocols/Macro.swift @@ -11,4 +11,8 @@ //===----------------------------------------------------------------------===// /// Describes a macro. -public protocol Macro {} +public protocol Macro { + /// How the resulting expansion should be formatted, `.auto` by default. + /// Use `.disabled` for the expansion to be used as is. + static var formatMode: FormatMode { get } +} diff --git a/lit_tests/compiler_plugin_basic.swift b/lit_tests/compiler_plugin_basic.swift index c71b2ca7f40..db3608de860 100644 --- a/lit_tests/compiler_plugin_basic.swift +++ b/lit_tests/compiler_plugin_basic.swift @@ -23,7 +23,9 @@ class MyClass { } // For '@Metadata' -// CHECK: static var __metadata__: [String: String] { ["name": "MyClass"] } +// CHECK: {{^}}static var __metadata__: [String: String] { +// CHECK-NEXT: {{^}} ["name": "MyClass"] +// CHECK-NEXT: {{^}}} // For '#echo(12)' // CHECK: /* echo */12