From 760996d8b8cca767fe0cfd2a29b27f526b9b30c5 Mon Sep 17 00:00:00 2001 From: Ben Barham Date: Tue, 25 Apr 2023 11:19:17 -0700 Subject: [PATCH] Automatically format expanded macros Rather than requiring macro implementations to add required whitespace and indentation, basic format all macro expansions. Right now this uses the default four space indentation, we can consider having that inferred later. Macros can opt-out of automatic formatting by implementing `formatMode` and setting it to `.disabled`. Add new `trimmed(matching:)` and `trimmedDescription(matching:)` methods to allow eg. only trimming whitespace rather than all trivia. Use this when trimming macro expansions so that any added comments are kept. Resolves rdar://107731047. (cherry picked from commit e40c82c2b3f830db44bf1c483304f7877d726179) --- .../CMakeLists.txt | 1 + .../Macros.swift | 30 ++++++++++++++----- Sources/SwiftSyntax/Syntax.swift | 30 ++++++++++++++++--- Sources/SwiftSyntaxMacros/CMakeLists.txt | 1 + .../MacroProtocols/Macro+Format.swift | 29 ++++++++++++++++++ .../MacroProtocols/Macro.swift | 6 +++- lit_tests/compiler_plugin_basic.swift | 4 ++- 7 files changed, 88 insertions(+), 13 deletions(-) create mode 100644 Sources/SwiftSyntaxMacros/MacroProtocols/Macro+Format.swift 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