Skip to content

Commit 7bc8dda

Browse files
committed
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`. Update `trimmed` and `trimmedDescription` to only trim whitespace leading and trailing whitespace, rather than removing the trivia entirely. This allows macros to add comments to their expansions. Resolves rdar://107731047.
1 parent a648679 commit 7bc8dda

File tree

5 files changed

+68
-14
lines changed

5 files changed

+68
-14
lines changed

Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ add_swift_host_library(SwiftCompilerPluginMessageHandling
1616

1717
target_link_libraries(SwiftCompilerPluginMessageHandling PUBLIC
1818
SwiftSyntax
19+
SwiftBasicFormat
1920
SwiftDiagnostics
2021
SwiftParser
2122
SwiftSyntaxMacros

Sources/SwiftCompilerPluginMessageHandling/Macros.swift

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import SwiftBasicFormat
1314
import SwiftDiagnostics
1415
import SwiftSyntax
1516
import SwiftSyntaxMacros
@@ -49,21 +50,21 @@ extension CompilerPluginMessageHandler {
4950
try exprMacroDef.expansion(of: node, in: context)
5051
}
5152
let rewritten = try _openExistential(macroSyntax, do: _expand)
52-
expandedSource = rewritten.description
53+
expandedSource = rewritten.formattedExpansion(macroDefinition.formatMode)
5354

5455
case let declMacroDef as DeclarationMacro.Type:
5556
func _expand<Node: FreestandingMacroExpansionSyntax>(node: Node) throws -> [DeclSyntax] {
5657
try declMacroDef.expansion(of: node, in: context)
5758
}
5859
let rewritten = try _openExistential(macroSyntax, do: _expand)
59-
expandedSource = CodeBlockItemListSyntax(rewritten.map { CodeBlockItemSyntax(item: .decl($0)) }).description
60+
expandedSource = CodeBlockItemListSyntax(rewritten.map { CodeBlockItemSyntax(item: .decl($0)) }).formattedExpansion(macroDefinition.formatMode)
6061

6162
case let codeItemMacroDef as CodeItemMacro.Type:
6263
func _expand<Node: FreestandingMacroExpansionSyntax>(node: Node) throws -> [CodeBlockItemSyntax] {
6364
try codeItemMacroDef.expansion(of: node, in: context)
6465
}
6566
let rewritten = try _openExistential(macroSyntax, do: _expand)
66-
expandedSource = CodeBlockItemListSyntax(rewritten).description
67+
expandedSource = CodeBlockItemListSyntax(rewritten).formattedExpansion(macroDefinition.formatMode)
6768

6869
default:
6970
throw MacroExpansionError.unmathedMacroRole
@@ -113,7 +114,7 @@ extension CompilerPluginMessageHandler {
113114
in: context
114115
)
115116
expandedSources = accessors.map {
116-
$0.trimmedDescription
117+
$0.formattedExpansion(macroDefinition.formatMode)
117118
}
118119

119120
case (let attachedMacro as MemberAttributeMacro.Type, .memberAttribute):
@@ -145,7 +146,7 @@ extension CompilerPluginMessageHandler {
145146

146147
// Form a buffer containing an attribute list to return to the caller.
147148
expandedSources = attributes.map {
148-
$0.trimmedDescription
149+
$0.formattedExpansion(macroDefinition.formatMode)
149150
}
150151

151152
case (let attachedMacro as MemberMacro.Type, .member):
@@ -170,7 +171,7 @@ extension CompilerPluginMessageHandler {
170171
let members = try _openExistential(declGroup, do: expandMemberMacro)
171172

172173
// Form a buffer of member declarations to return to the caller.
173-
expandedSources = members.map { $0.trimmedDescription }
174+
expandedSources = members.map { $0.formattedExpansion(macroDefinition.formatMode) }
174175

175176
case (let attachedMacro as PeerMacro.Type, .peer):
176177
let peers = try attachedMacro.expansion(
@@ -181,7 +182,7 @@ extension CompilerPluginMessageHandler {
181182

182183
// Form a buffer of peer declarations to return to the caller.
183184
expandedSources = peers.map {
184-
$0.trimmedDescription
185+
$0.formattedExpansion(macroDefinition.formatMode)
185186
}
186187

187188
case (let attachedMacro as ConformanceMacro.Type, .conformance):
@@ -234,3 +235,17 @@ extension CompilerPluginMessageHandler {
234235
)
235236
}
236237
}
238+
239+
fileprivate extension SyntaxProtocol {
240+
/// Perform a format if required and then trim any leading/trailing
241+
/// whitespace.
242+
func formattedExpansion(_ mode: FormatMode) -> String {
243+
let formatted: Syntax
244+
if mode == .auto {
245+
formatted = self.formatted()
246+
} else {
247+
formatted = Syntax(self)
248+
}
249+
return formatted.trimmedDescription
250+
}
251+
}

Sources/SwiftSyntax/Syntax.swift

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -534,18 +534,24 @@ public extension SyntaxProtocol {
534534
data.raw.write(to: &target)
535535
}
536536

537-
/// A copy of this node without the leading trivia of the first token in the
538-
/// node and the trailing trivia of the last token in the node.
537+
/// A copy of this node with leading whitespace of the first token and
538+
/// trailing whitespace of the last token removed.
539539
var trimmed: Self {
540540
// TODO: Should only need one new node here
541-
return self.with(\.leadingTrivia, []).with(\.trailingTrivia, [])
541+
return self.with(
542+
\.leadingTrivia,
543+
Trivia(pieces: leadingTrivia.pieces.drop(while: { $0.isWhitespace }))
544+
).with(
545+
\.trailingTrivia,
546+
Trivia(pieces: trailingTrivia.pieces.reversed().drop(while: { $0.isWhitespace }).reversed())
547+
)
542548
}
543549

544-
/// The description of this node without the leading trivia of the first token
545-
/// in the node and the trailing trivia of the last token in the node.
550+
/// The description of this node with leading whitespace of the first token
551+
/// and trailing whitespace of the last token removed.
546552
var trimmedDescription: String {
547-
// TODO: We shouldn't need to create to copies just to get the description
548-
// without trivia.
553+
// TODO: We shouldn't need to create to copies just to get the trimmed
554+
// description.
549555
return self.trimmed.description
550556
}
551557
}

Sources/SwiftSyntaxMacros/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ add_swift_host_library(SwiftSyntaxMacros
1515
MacroProtocols/ExpressionMacro.swift
1616
MacroProtocols/FreestandingMacro.swift
1717
MacroProtocols/Macro.swift
18+
MacroProtocols/Macro+Format.swift
1819
MacroProtocols/MemberAttributeMacro.swift
1920
MacroProtocols/MemberMacro.swift
2021
MacroProtocols/PeerMacro.swift
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 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+
/// Describes the mode to use to format the result of an expansion.
14+
public enum FormatMode {
15+
/// Perform a basic format of the expansion. This is primarily for inserting
16+
/// whitespace as required (eg. between two keywords), but also adds simple
17+
/// newline and indentation.
18+
case auto
19+
20+
/// Disable automatically formatting the expanded macro. Trivia must be
21+
/// manually inserted where required (eg. adding spaces between keywords).
22+
case disabled
23+
}
24+
25+
public extension Macro {
26+
/// How the resulting expansion should be formatted, `.auto` by default.
27+
/// Use `.disabled` for the expansion to be used as is.
28+
static var formatMode: FormatMode {
29+
return .auto
30+
}
31+
}

0 commit comments

Comments
 (0)