Skip to content

Commit 824b318

Browse files
authored
Merge pull request #1845 from rintaro/macros-plugins-collappsed-attached-result
[Macros] Attached macro expansions return single string
2 parents 7085322 + 6fc14cb commit 824b318

File tree

10 files changed

+189
-18
lines changed

10 files changed

+189
-18
lines changed

Package.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,17 @@ let package = Package(
180180

181181
.target(
182182
name: "SwiftSyntaxMacroExpansion",
183-
dependencies: ["SwiftSyntax", "SwiftSyntaxMacros"],
183+
dependencies: ["SwiftSyntax", "SwiftSyntaxMacros", "SwiftDiagnostics"],
184184
exclude: ["CMakeLists.txt"]
185185
),
186186

187+
.testTarget(
188+
name: "SwiftSyntaxMacroExpansionTest",
189+
dependencies: [
190+
"SwiftSyntax", "_SwiftSyntaxTestSupport", "SwiftSyntaxMacroExpansion", "SwiftSyntaxBuilder",
191+
]
192+
),
193+
187194
// MARK: SwiftSyntaxMacrosTestSupport
188195

189196
.target(

Sources/SwiftCompilerPluginMessageHandling/CompilerPluginMessageHandler.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@ public protocol MessageConnection {
4040
func waitForNextMessage<RX: Decodable>(_ type: RX.Type) throws -> RX?
4141
}
4242

43+
/// Represent the capability of the plugin host (i.e. compiler).
44+
struct HostCapability {
45+
var protocolVersion: Int
46+
47+
// Create an "oldest" capability.
48+
init() {
49+
self.protocolVersion = 0
50+
}
51+
52+
init(_ message: PluginMessage.HostCapability) {
53+
self.protocolVersion = message.protocolVersion
54+
}
55+
56+
/// Compiler accept 'expandMacroResult' response message.
57+
var hasExpandMacroResult: Bool { protocolVersion >= 5 }
58+
}
59+
4360
/// 'CompilerPluginMessageHandler' is a type that listens to the message
4461
/// connection and dispatches them to the actual plugin provider, then send back
4562
/// the response.
@@ -52,9 +69,13 @@ public class CompilerPluginMessageHandler<Connection: MessageConnection, Provide
5269
/// Object to provide actual plugin functions.
5370
let provider: Provider
5471

72+
/// Plugin host capability
73+
var hostCapability: HostCapability
74+
5575
public init(connection: Connection, provider: Provider) {
5676
self.connection = connection
5777
self.provider = provider
78+
self.hostCapability = HostCapability()
5879
}
5980
}
6081

@@ -80,7 +101,13 @@ extension CompilerPluginMessageHandler {
80101
/// Handles a single message received from the plugin host.
81102
fileprivate func handleMessage(_ message: HostToPluginMessage) throws {
82103
switch message {
83-
case .getCapability:
104+
case .getCapability(let hostCapability):
105+
// Remember the peer capability if provided.
106+
if let hostCapability = hostCapability {
107+
self.hostCapability = .init(hostCapability)
108+
}
109+
110+
// Return the plugin capability.
84111
let capability = PluginMessage.PluginCapability(
85112
protocolVersion: PluginMessage.PROTOCOL_VERSION_NUMBER,
86113
features: provider.features.map({ $0.rawValue })

Sources/SwiftCompilerPluginMessageHandling/Macros.swift

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,15 @@ extension CompilerPluginMessageHandler {
6868
let diagnostics = context.diagnostics.map {
6969
PluginMessage.Diagnostic(from: $0, in: sourceManager)
7070
}
71-
try self.sendMessage(
72-
.expandFreestandingMacroResult(expandedSource: expandedSource, diagnostics: diagnostics)
73-
)
71+
72+
let response: PluginToHostMessage
73+
if hostCapability.hasExpandMacroResult {
74+
response = .expandMacroResult(expandedSource: expandedSource, diagnostics: diagnostics)
75+
} else {
76+
// TODO: Remove this when all compilers have 'hasExpandMacroResult'.
77+
response = .expandFreestandingMacroResult(expandedSource: expandedSource, diagnostics: diagnostics)
78+
}
79+
try self.sendMessage(response)
7480
}
7581

7682
/// Expand `@attached(XXX)` macros.
@@ -95,20 +101,34 @@ extension CompilerPluginMessageHandler {
95101
let declarationNode = sourceManager.add(declSyntax).cast(DeclSyntax.self)
96102
let parentDeclNode = parentDeclSyntax.map { sourceManager.add($0).cast(DeclSyntax.self) }
97103

104+
// TODO: Make this a 'String?' and remove non-'hasExpandMacroResult' branches.
98105
let expandedSources: [String]?
99106
do {
100107
guard let macroDefinition = resolveMacro(macro) else {
101108
throw MacroExpansionError.macroTypeNotFound(macro)
102109
}
110+
let role = MacroRole(messageMacroRole: macroRole)
103111

104-
expandedSources = SwiftSyntaxMacroExpansion.expandAttachedMacro(
112+
let expansions = SwiftSyntaxMacroExpansion.expandAttachedMacroWithoutCollapsing(
105113
definition: macroDefinition,
106-
macroRole: MacroRole(messageMacroRole: macroRole),
114+
macroRole: role,
107115
attributeNode: attributeNode,
108116
declarationNode: declarationNode,
109117
parentDeclNode: parentDeclNode,
110118
in: context
111119
)
120+
if let expansions, hostCapability.hasExpandMacroResult {
121+
// Make a single element array by collapsing the results into a string.
122+
expandedSources = [
123+
SwiftSyntaxMacroExpansion.collapse(
124+
expansions: expansions,
125+
for: role,
126+
attachedTo: declarationNode
127+
)
128+
]
129+
} else {
130+
expandedSources = expansions
131+
}
112132
} catch {
113133
context.addDiagnostics(from: error, node: attributeNode)
114134
expandedSources = nil
@@ -117,9 +137,14 @@ extension CompilerPluginMessageHandler {
117137
let diagnostics = context.diagnostics.map {
118138
PluginMessage.Diagnostic(from: $0, in: sourceManager)
119139
}
120-
try self.sendMessage(
121-
.expandAttachedMacroResult(expandedSources: expandedSources, diagnostics: diagnostics)
122-
)
140+
141+
let response: PluginToHostMessage
142+
if hostCapability.hasExpandMacroResult {
143+
response = .expandMacroResult(expandedSource: expandedSources?.first, diagnostics: diagnostics)
144+
} else {
145+
response = .expandAttachedMacroResult(expandedSources: expandedSources, diagnostics: diagnostics)
146+
}
147+
try self.sendMessage(response)
123148
}
124149
}
125150

Sources/SwiftCompilerPluginMessageHandling/PluginMessages.swift

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
// NOTE: Types in this file should be self-contained and should not depend on any non-stdlib types.
1515

1616
internal enum HostToPluginMessage: Codable {
17-
/// Get capability of this plugin.
18-
case getCapability
17+
/// Send capability of the host, and get capability of the plugin.
18+
case getCapability(
19+
capability: PluginMessage.HostCapability?
20+
)
1921

2022
/// Expand a '@freestanding' macro.
2123
case expandFreestandingMacro(
@@ -49,11 +51,19 @@ internal enum PluginToHostMessage: Codable {
4951
capability: PluginMessage.PluginCapability
5052
)
5153

54+
/// Unified response for freestanding/attached macro expansion.
55+
case expandMacroResult(
56+
expandedSource: String?,
57+
diagnostics: [PluginMessage.Diagnostic]
58+
)
59+
60+
// @available(*, deprecated: "use expandMacroResult() instead")
5261
case expandFreestandingMacroResult(
5362
expandedSource: String?,
5463
diagnostics: [PluginMessage.Diagnostic]
5564
)
5665

66+
// @available(*, deprecated: "use expandMacroResult() instead")
5767
case expandAttachedMacroResult(
5868
expandedSources: [String]?,
5969
diagnostics: [PluginMessage.Diagnostic]
@@ -66,7 +76,11 @@ internal enum PluginToHostMessage: Codable {
6676
}
6777

6878
/*namespace*/ internal enum PluginMessage {
69-
static var PROTOCOL_VERSION_NUMBER: Int { 4 } // Added 'loadPluginLibrary'.
79+
static var PROTOCOL_VERSION_NUMBER: Int { 5 } // Added 'expandMacroResult'.
80+
81+
struct HostCapability: Codable {
82+
var protocolVersion: Int
83+
}
7084

7185
struct PluginCapability: Codable {
7286
var protocolVersion: Int

Sources/SwiftSyntaxMacroExpansion/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
add_swift_host_library(SwiftSyntaxMacroExpansion
2+
FunctionParameterUtils.swift
23
MacroExpansion.swift
4+
MacroReplacement.swift
35
)
46

57
target_link_libraries(SwiftSyntaxMacroExpansion PUBLIC

Sources/SwiftSyntaxMacroExpansion/MacroExpansion.swift

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 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+
113
import SwiftSyntax
214
@_spi(MacroExpansion) import SwiftSyntaxMacros
315

@@ -160,7 +172,7 @@ public func expandFreestandingMacro(
160172
/// - Returns: A list of expanded source text. Upon failure (i.e.
161173
/// `defintion.expansion()` throws) returns `nil`, and the diagnostics
162174
/// representing the `Error` are guaranteed to be added to context.
163-
public func expandAttachedMacro<Context: MacroExpansionContext>(
175+
public func expandAttachedMacroWithoutCollapsing<Context: MacroExpansionContext>(
164176
definition: Macro.Type,
165177
macroRole: MacroRole,
166178
attributeNode: AttributeSyntax,
@@ -292,6 +304,40 @@ public func expandAttachedMacro<Context: MacroExpansionContext>(
292304
}
293305
}
294306

307+
/// Expand `@attached(XXX)` macros.
308+
///
309+
/// - Parameters:
310+
/// - definition: a type that conforms to one or more attached `Macro` protocols.
311+
/// - macroRole: indicates which `Macro` protocol expansion should be performed
312+
/// - attributeNode: attribute syntax node (e.g. `@macroName(argument)`).
313+
/// - declarationNode: target declaration syntax node to apply the expansion.
314+
/// - parentDeclNode: Only used for `MacroRole.memberAttribute`. The parent
315+
/// context node of `declarationNode`.
316+
/// - in: context of the expansion.
317+
/// - Returns: expanded source text. Upon failure (i.e. `defintion.expansion()`
318+
/// throws) returns `nil`, and the diagnostics representing the `Error` are
319+
/// guaranteed to be added to context.
320+
public func expandAttachedMacro<Context: MacroExpansionContext>(
321+
definition: Macro.Type,
322+
macroRole: MacroRole,
323+
attributeNode: AttributeSyntax,
324+
declarationNode: DeclSyntax,
325+
parentDeclNode: DeclSyntax?,
326+
in context: Context
327+
) -> String? {
328+
let expandedSources = expandAttachedMacroWithoutCollapsing(
329+
definition: definition,
330+
macroRole: macroRole,
331+
attributeNode: attributeNode,
332+
declarationNode: declarationNode,
333+
parentDeclNode: parentDeclNode,
334+
in: context
335+
)
336+
return expandedSources.map {
337+
collapse(expansions: $0, for: macroRole, attachedTo: declarationNode)
338+
}
339+
}
340+
295341
fileprivate extension SyntaxProtocol {
296342
/// Perform a format if required and then trim any leading/trailing
297343
/// whitespace.
@@ -306,3 +352,56 @@ fileprivate extension SyntaxProtocol {
306352
return formatted.trimmedDescription(matching: { $0.isWhitespace })
307353
}
308354
}
355+
356+
/// Join `expansions`
357+
public func collapse<Node: SyntaxProtocol>(
358+
expansions: [String],
359+
for role: MacroRole,
360+
attachedTo declarationNode: Node
361+
) -> String {
362+
if expansions.isEmpty {
363+
return ""
364+
}
365+
366+
var expansions = expansions
367+
var separator: String = "\n\n"
368+
369+
if role == .accessor,
370+
let varDecl = declarationNode.as(VariableDeclSyntax.self),
371+
let binding = varDecl.bindings.first,
372+
binding.accessor == nil
373+
{
374+
let indentation = String(repeating: " ", count: 4)
375+
376+
expansions = expansions.map({ indent($0, with: indentation) })
377+
expansions[0] = "{\n" + expansions[0]
378+
expansions[expansions.count - 1] += "\n}"
379+
} else if role == .memberAttribute {
380+
separator = " "
381+
}
382+
383+
return expansions.joined(separator: separator)
384+
}
385+
386+
fileprivate func indent(_ source: String, with indentation: String) -> String {
387+
if source.isEmpty || indentation.isEmpty {
388+
return source
389+
}
390+
391+
var indented = ""
392+
var remaining = source[...]
393+
while let nextNewline = remaining.firstIndex(where: { $0.isNewline }) {
394+
if nextNewline != remaining.startIndex {
395+
indented += indentation
396+
}
397+
indented += remaining[...nextNewline]
398+
remaining = remaining[remaining.index(after: nextNewline)...]
399+
}
400+
401+
if !remaining.isEmpty {
402+
indented += indentation
403+
indented += remaining
404+
}
405+
406+
return indented
407+
}

Sources/SwiftSyntaxMacros/CMakeLists.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ add_swift_host_library(SwiftSyntaxMacros
2222

2323
AbstractSourceLocation.swift
2424
BasicMacroExpansionContext.swift
25-
FunctionParameterUtils.swift
2625
MacroExpansionContext.swift
27-
MacroReplacement.swift
2826
MacroSystem.swift
2927
Syntax+MacroEvaluation.swift
3028
)

Tests/SwiftSyntaxMacrosTest/MacroReplacementTests.swift renamed to Tests/SwiftSyntaxMacroExpansionTest/MacroReplacementTests.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import SwiftDiagnostics
14-
import SwiftParser
1514
import SwiftSyntax
1615
import SwiftSyntaxBuilder
17-
import SwiftSyntaxMacros
16+
import SwiftSyntaxMacroExpansion
1817
import _SwiftSyntaxTestSupport
1918
import XCTest
2019

0 commit comments

Comments
 (0)