Skip to content

Commit dbfabfc

Browse files
committed
[Macros] Add executable compiler plugin support library
'SwiftCompilerPlugin' is a helper library to write an executable compiler plugin. Usage: import SwiftCompilerPlugin import SwiftSyntaxMacros @main struct MyPlugin: CompilerPlugin { var providingMacros: [Macro.Type] = [MyMacro.self] } struct MyMacro: ExpressionMacro { ... }
1 parent bcaad6d commit dbfabfc

File tree

6 files changed

+984
-0
lines changed

6 files changed

+984
-0
lines changed

Package.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ let package = Package(
4343
.library(name: "SwiftSyntaxParser", type: .static, targets: ["SwiftSyntaxParser"]),
4444
.library(name: "SwiftSyntaxBuilder", type: .static, targets: ["SwiftSyntaxBuilder"]),
4545
.library(name: "SwiftSyntaxMacros", type: .static, targets: ["SwiftSyntaxMacros"]),
46+
.library(name: "SwiftCompilerPlugin", type: .static, targets: ["SwiftCompilerPlugin"]),
4647
.library(name: "SwiftRefactor", type: .static, targets: ["SwiftRefactor"]),
4748
],
4849
targets: [
@@ -122,6 +123,12 @@ let package = Package(
122123
"CMakeLists.txt"
123124
]
124125
),
126+
.target(
127+
name: "SwiftCompilerPlugin",
128+
dependencies: [
129+
"SwiftSyntax", "SwiftParser", "SwiftDiagnostics", "SwiftSyntaxMacros", "SwiftOperators",
130+
]
131+
),
125132
.target(
126133
name: "SwiftRefactor",
127134
dependencies: [
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift 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 http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
// NOTE: This basic plugin mechanism is mostly copied from
13+
// https://github.com/apple/swift-package-manager/blob/main/Sources/PackagePlugin/Plugin.swift
14+
15+
import SwiftSyntaxMacros
16+
17+
@_implementationOnly import Foundation
18+
#if os(Windows)
19+
@_implementationOnly import ucrt
20+
#endif
21+
22+
//
23+
// This source file contains the main entry point for compiler plugins.
24+
// A plugin receives messages from the "plugin host" (typically
25+
// 'swift-frontend'), and sends back messages in return based on its actions.
26+
//
27+
// Depending on the platform, plugins are invoked in a sanbox that blocks
28+
// network access and prevents any file system changes.
29+
//
30+
// The host process and the plugin communicate using messages in the form of
31+
// length-prefixed JSON-encoded Swift enums. The host sends messages to the
32+
// plugin through its standard-input pipe, and receives messages through the
33+
// plugin's standard-output pipe. The plugin's standard-error is considered
34+
// to be free-form textual console output.
35+
//
36+
// Within the plugin process, `stdout` is redirected to `stderr` so that print
37+
// statements from the plugin are treated as plain-text output, and `stdin` is
38+
// closed so that any attemps by the plugin logic to read from console result
39+
// in errors instead of blocking the process. The original `stdin` and `stdout`
40+
// are duplicated for use as messaging pipes, and are not directly used by the
41+
// plugin logic.
42+
//
43+
// The exit code of the plugin process indicates whether the plugin invocation
44+
// is considered successful. A failure result should also be accompanied by an
45+
// emitted error diagnostic, so that errors are understandable by the user.
46+
//
47+
// Using standard input and output streams for messaging avoids having to make
48+
// allowances in the sandbox for other channels of communication, and seems a
49+
// more portable approach than many of the alternatives. This is all somewhat
50+
// temporary in any case — in the long term, something like distributed actors
51+
// or something similar can hopefully replace the custom messaging.
52+
//
53+
// Usage:
54+
// struct MyPlugin: CompilerPlugin {
55+
// var providingMacros: [Macros.Type] = [
56+
// StringifyMacro.self
57+
// ]
58+
public protocol CompilerPlugin {
59+
init()
60+
61+
var providingMacros: [Macro.Type] { get }
62+
}
63+
64+
extension CompilerPlugin {
65+
66+
/// Main entry point of the plugin — sets up a communication channel with
67+
/// the plugin host and runs the main message loop.
68+
public static func main() async throws {
69+
// Duplicate the `stdin` file descriptor, which we will then use for
70+
// receiving messages from the plugin host.
71+
let inputFD = dup(fileno(stdin))
72+
guard inputFD >= 0 else {
73+
internalError("Could not duplicate `stdin`: \(describe(errno: errno)).")
74+
}
75+
76+
// Having duplicated the original standard-input descriptor, we close
77+
// `stdin` so that attempts by the plugin to read console input (which
78+
// are usually a mistake) return errors instead of blocking.
79+
guard close(fileno(stdin)) >= 0 else {
80+
internalError("Could not close `stdin`: \(describe(errno: errno)).")
81+
}
82+
83+
// Duplicate the `stdout` file descriptor, which we will then use for
84+
// sending messages to the plugin host.
85+
let outputFD = dup(fileno(stdout))
86+
guard outputFD >= 0 else {
87+
internalError("Could not dup `stdout`: \(describe(errno: errno)).")
88+
}
89+
90+
// Having duplicated the original standard-output descriptor, redirect
91+
// `stdout` to `stderr` so that all free-form text output goes there.
92+
guard dup2(fileno(stderr), fileno(stdout)) >= 0 else {
93+
internalError("Could not dup2 `stdout` to `stderr`: \(describe(errno: errno)).")
94+
}
95+
96+
// Turn off full buffering so printed text appears as soon as possible.
97+
// Windows is much less forgiving than other platforms. If line
98+
// buffering is enabled, we must provide a buffer and the size of the
99+
// buffer. As a result, on Windows, we completely disable all
100+
// buffering, which means that partial writes are possible.
101+
#if os(Windows)
102+
setvbuf(stdout, nil, _IONBF, 0)
103+
#else
104+
setvbuf(stdout, nil, _IOLBF, 0)
105+
#endif
106+
107+
// Open a message channel for communicating with the plugin host.
108+
pluginHostConnection = PluginHostConnection(
109+
inputStream: FileHandle(fileDescriptor: inputFD),
110+
outputStream: FileHandle(fileDescriptor: outputFD)
111+
)
112+
113+
// Handle messages from the host until the input stream is closed,
114+
// indicating that we're done.
115+
let instance = Self()
116+
do {
117+
while let message = try pluginHostConnection.waitForNextMessage() {
118+
try await instance.handleMessage(message)
119+
}
120+
} catch {
121+
// Emit a diagnostic and indicate failure to the plugin host,
122+
// and exit with an error code.
123+
internalError(String(describing: error))
124+
}
125+
}
126+
127+
// Private function to report internal errors and then exit.
128+
fileprivate static func internalError(_ message: String) -> Never {
129+
fputs("Internal Error: \(message)\n", stderr)
130+
exit(1)
131+
}
132+
133+
// Private function to construct an error message from an `errno` code.
134+
fileprivate static func describe(errno: Int32) -> String {
135+
if let cStr = strerror(errno) { return String(cString: cStr) }
136+
return String(describing: errno)
137+
}
138+
139+
/// Handles a single message received from the plugin host.
140+
fileprivate func handleMessage(_ message: HostToPluginMessage) async throws {
141+
switch message {
142+
case .getCapability:
143+
try pluginHostConnection.sendMessage(
144+
.getCapabilityResult(capability: PluginMessage.capability)
145+
)
146+
break
147+
148+
case .expandFreestandingMacro(let macro, let discriminator, let expandingSyntax):
149+
try expandFreestandingMacro(
150+
macro: macro,
151+
discriminator: discriminator,
152+
expandingSyntax: expandingSyntax
153+
)
154+
155+
case .expandAttachedMacro(let macro, let macroRole, let discriminator, let customAttributeSyntax, let declSyntax, let parentDeclSyntax):
156+
try expandAttachedMacro(
157+
macro: macro,
158+
macroRole: macroRole,
159+
discriminator: discriminator,
160+
customAttributeSyntax: customAttributeSyntax,
161+
declSyntax: declSyntax,
162+
parentDeclSyntax: parentDeclSyntax
163+
)
164+
}
165+
}
166+
}
167+
168+
/// Message channel for bidirectional communication with the plugin host.
169+
internal fileprivate(set) var pluginHostConnection: PluginHostConnection!
170+
171+
typealias PluginHostConnection = MessageConnection<PluginToHostMessage, HostToPluginMessage>
172+
173+
internal struct MessageConnection<TX, RX> where TX: Encodable, RX: Decodable {
174+
let inputStream: FileHandle
175+
let outputStream: FileHandle
176+
177+
func sendMessage(_ message: TX) throws {
178+
// Encode the message as JSON.
179+
let payload = try JSONEncoder().encode(message)
180+
181+
// Write the header (a 64-bit length field in little endian byte order).
182+
var count = UInt64(payload.count).littleEndian
183+
let header = Swift.withUnsafeBytes(of: &count) { Data($0) }
184+
assert(header.count == 8)
185+
186+
// Write the header and payload.
187+
try outputStream._write(contentsOf: header)
188+
try outputStream._write(contentsOf: payload)
189+
}
190+
191+
func waitForNextMessage() throws -> RX? {
192+
// Read the header (a 64-bit length field in little endian byte order).
193+
guard
194+
let header = try inputStream._read(upToCount: 8),
195+
header.count != 0
196+
else {
197+
return nil
198+
}
199+
guard header.count == 8 else {
200+
throw PluginMessageError.truncatedHeader
201+
}
202+
203+
// Decode the count.
204+
let count = header.withUnsafeBytes {
205+
UInt64(littleEndian: $0.load(as: UInt64.self))
206+
}
207+
guard count >= 2 else {
208+
throw PluginMessageError.invalidPayloadSize
209+
}
210+
211+
// Read the JSON payload.
212+
guard
213+
let payload = try inputStream._read(upToCount: Int(count)),
214+
payload.count == count
215+
else {
216+
throw PluginMessageError.truncatedPayload
217+
}
218+
219+
// Decode and return the message.
220+
return try JSONDecoder().decode(RX.self, from: payload)
221+
}
222+
223+
enum PluginMessageError: Swift.Error {
224+
case truncatedHeader
225+
case invalidPayloadSize
226+
case truncatedPayload
227+
}
228+
}
229+
230+
private extension FileHandle {
231+
func _write(contentsOf data: Data) throws {
232+
if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) {
233+
return try self.write(contentsOf: data)
234+
} else {
235+
return self.write(data)
236+
}
237+
}
238+
239+
func _read(upToCount count: Int) throws -> Data? {
240+
if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) {
241+
return try self.read(upToCount: count)
242+
} else {
243+
return self.readData(ofLength: 8)
244+
}
245+
}
246+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift 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 http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftDiagnostics
14+
import SwiftSyntax
15+
16+
/// Errors in macro handing.
17+
enum MacroExpansionError: String {
18+
case macroTypeNotFound = "macro expanding type not found"
19+
case freestandingMacroSyntaxIsNotMacro = "macro syntax couldn't be parsed"
20+
}
21+
22+
extension MacroExpansionError: DiagnosticMessage {
23+
var message: String {
24+
self.rawValue
25+
}
26+
var diagnosticID: SwiftDiagnostics.MessageID {
27+
.init(domain: "\(type(of: self))", id: "\(self)")
28+
}
29+
var severity: SwiftDiagnostics.DiagnosticSeverity {
30+
.error
31+
}
32+
}
33+
34+
extension PluginMessage.Diagnostic.Severity {
35+
init(from syntaxDiagSeverity: SwiftDiagnostics.DiagnosticSeverity) {
36+
switch syntaxDiagSeverity {
37+
case .error: self = .error
38+
case .warning: self = .warning
39+
case .note: self = .note
40+
}
41+
}
42+
}
43+
44+
extension PluginMessage.Diagnostic {
45+
init(from syntaxDiag: SwiftDiagnostics.Diagnostic, in sourceManager: SourceManager) {
46+
guard
47+
let position = sourceManager.position(
48+
of: syntaxDiag.node,
49+
at: .afterLeadingTrivia
50+
)
51+
else {
52+
fatalError("unknown diagnostic node")
53+
}
54+
55+
self.position = .init(fileName: position.fileName, offset: position.utf8Offset)
56+
self.severity = .init(from: syntaxDiag.diagMessage.severity)
57+
self.message = syntaxDiag.message
58+
59+
self.highlights = syntaxDiag.highlights.map {
60+
guard let range = sourceManager.range(of: $0) else {
61+
fatalError("highlight node is not known")
62+
}
63+
return .init(
64+
fileName: range.fileName,
65+
startOffset: range.startUTF8Offset,
66+
endOffset: range.endUTF8Offset
67+
)
68+
}
69+
70+
self.notes = syntaxDiag.notes.map {
71+
guard let pos = sourceManager.position(of: $0.node, at: .afterLeadingTrivia) else {
72+
fatalError("note node is not known")
73+
}
74+
let position = PluginMessage.Diagnostic.Position(
75+
fileName: pos.fileName,
76+
offset: pos.utf8Offset
77+
)
78+
return .init(position: position, message: $0.message)
79+
}
80+
81+
self.fixIts = syntaxDiag.fixIts.map {
82+
PluginMessage.Diagnostic.FixIt(
83+
message: $0.message.message,
84+
changes: $0.changes.changes.map {
85+
let range: SourceManager.SourceRange?
86+
let text: String
87+
switch $0 {
88+
case .replace(let oldNode, let newNode):
89+
range = sourceManager.range(
90+
of: oldNode,
91+
from: .afterLeadingTrivia,
92+
to: .beforeTrailingTrivia
93+
)
94+
text = newNode.trimmedDescription
95+
case .replaceLeadingTrivia(let token, let newTrivia):
96+
range = sourceManager.range(
97+
of: Syntax(token),
98+
from: .beforeLeadingTrivia,
99+
to: .afterLeadingTrivia
100+
)
101+
text = newTrivia.description
102+
case .replaceTrailingTrivia(let token, let newTrivia):
103+
range = sourceManager.range(
104+
of: Syntax(token),
105+
from: .beforeTrailingTrivia,
106+
to: .afterTrailingTrivia
107+
)
108+
text = newTrivia.description
109+
}
110+
guard let range = range else {
111+
fatalError("unknown")
112+
}
113+
return .init(
114+
range: PositionRange(
115+
fileName: range.fileName,
116+
startOffset: range.startUTF8Offset,
117+
endOffset: range.endUTF8Offset
118+
),
119+
newText: text
120+
)
121+
}
122+
)
123+
}
124+
}
125+
}

0 commit comments

Comments
 (0)