@@ -45,6 +45,7 @@ public final class InitPackage {
45
45
case executable = " executable "
46
46
case tool = " tool "
47
47
case `extension` = " extension "
48
+ case macro = " macro "
48
49
49
50
public var description : String {
50
51
return rawValue
@@ -135,6 +136,16 @@ public final class InitPackage {
135
136
136
137
import PackageDescription
137
138
139
+ """
140
+
141
+ if packageType == . macro {
142
+ stream <<< """
143
+ import CompilerPluginSupport
144
+
145
+ """
146
+ }
147
+
148
+ stream <<< """
138
149
let package = Package(
139
150
140
151
"""
@@ -144,8 +155,29 @@ public final class InitPackage {
144
155
name: " \( pkgname) "
145
156
""" )
146
157
158
+ var platforms = options. platforms
159
+
160
+ // Macros require macOS 10.15, iOS 13, etc.
161
+ if packageType == . macro {
162
+ func addIfMissing( _ newPlatform: SupportedPlatform ) {
163
+ if platforms. contains ( where: { platform in
164
+ platform. platform == newPlatform. platform
165
+ } ) {
166
+ return
167
+ }
168
+
169
+ platforms. append ( newPlatform)
170
+ }
171
+
172
+ addIfMissing ( . init( platform: . macOS, version: . init( " 10.15 " ) ) )
173
+ addIfMissing ( . init( platform: . iOS, version: . init( " 13 " ) ) )
174
+ addIfMissing ( . init( platform: . tvOS, version: . init( " 13 " ) ) )
175
+ addIfMissing ( . init( platform: . watchOS, version: . init( " 6 " ) ) )
176
+ addIfMissing ( . init( platform: . macCatalyst, version: . init( " 13 " ) ) )
177
+ }
178
+
147
179
var platformsParams = [ String] ( )
148
- for supportedPlatform in options . platforms {
180
+ for supportedPlatform in platforms {
149
181
let version = supportedPlatform. version
150
182
let platform = supportedPlatform. platform
151
183
@@ -165,7 +197,7 @@ public final class InitPackage {
165
197
}
166
198
167
199
// Package platforms
168
- if !options . platforms. isEmpty {
200
+ if !platforms. isEmpty {
169
201
pkgParams. append ( """
170
202
platforms: [ \( platformsParams. joined ( separator: " , " ) ) ]
171
203
""" )
@@ -181,6 +213,20 @@ public final class InitPackage {
181
213
targets: [ " \( pkgname) " ]),
182
214
]
183
215
""" )
216
+ } else if packageType == . macro {
217
+ pkgParams. append ( """
218
+ products: [
219
+ // Products define the executables and libraries a package produces, making them visible to other packages.
220
+ .library(
221
+ name: " \( pkgname) " ,
222
+ targets: [ " \( pkgname) " ]),
223
+ .executable(
224
+ name: " \( pkgname) Client " ,
225
+ targets: [ " \( pkgname) Client " ]
226
+ ),
227
+ ]
228
+ """ )
229
+
184
230
}
185
231
186
232
// Package dependencies
@@ -190,6 +236,12 @@ public final class InitPackage {
190
236
.package(url: " https://github.com/apple/swift-argument-parser.git " , from: " 1.2.0 " ),
191
237
]
192
238
""" )
239
+ } else if packageType == . macro {
240
+ pkgParams. append ( """
241
+ dependencies: [
242
+ .package(url: " https://github.com/apple/swift-syntax.git " , branch: " main " ),
243
+ ]
244
+ """ )
193
245
}
194
246
195
247
// Package targets
@@ -219,6 +271,32 @@ public final class InitPackage {
219
271
path: " Sources " ),
220
272
]
221
273
"""
274
+ } else if packageType == . macro {
275
+ param += """
276
+ // Macro implementation, only built for the host and never part of a client program.
277
+ .macro(name: " \( pkgname) Macros " ,
278
+ dependencies: [
279
+ .product(name: " SwiftSyntaxMacros " , package: " swift-syntax " ),
280
+ .product(name: " SwiftCompilerPlugin " , package: " swift-syntax " ),
281
+ ]
282
+ ),
283
+
284
+ // Library that exposes a macro as part of its API, which is used in client programs.
285
+ .target(name: " \( pkgname) " , dependencies: [ " \( pkgname) Macros " ]),
286
+
287
+ // A client of the library, which is able to use the macro in its
288
+ // own code.
289
+ .executableTarget(name: " \( pkgname) Client " , dependencies: [ " \( pkgname) " ]),
290
+
291
+ // A test target used to develop the macro implementation.
292
+ .testTarget(
293
+ name: " \( pkgname) Tests " ,
294
+ dependencies: [
295
+ " \( pkgname) Macros " ,
296
+ ]
297
+ ),
298
+ ]
299
+ """
222
300
} else {
223
301
param += """
224
302
.target(
@@ -239,7 +317,8 @@ public final class InitPackage {
239
317
// Create a tools version with current version but with patch set to zero.
240
318
// We do this to avoid adding unnecessary constraints to patch versions, if
241
319
// the package really needs it, they should add it manually.
242
- let version = InitPackage . newPackageToolsVersion. zeroedPatch
320
+ let version = packageType == . macro ? ToolsVersion . vNext
321
+ : InitPackage . newPackageToolsVersion. zeroedPatch
243
322
244
323
// Write the current tools version.
245
324
try ToolsVersionSpecificationWriter . rewriteSpecification (
@@ -331,13 +410,33 @@ public final class InitPackage {
331
410
}
332
411
}
333
412
"""
413
+ case . macro:
414
+ content = """
415
+ // The Swift Programming Language
416
+ // https://docs.swift.org/swift-book
417
+
418
+ // A macro that produces both a value and a string containing the
419
+ // source code that generated the value. For example,
420
+ //
421
+ // #stringify(x + y)
422
+ //
423
+ // produces a tuple `(x + y, " x + y " )`.
424
+ @freestanding(expression)
425
+ public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: " \( moduleName) Macros " , type: " StringifyMacro " )
426
+ """
427
+
334
428
case . empty, . `extension`:
335
429
throw InternalError ( " invalid packageType \( packageType) " )
336
430
}
337
431
338
432
try writePackageFile ( sourceFile) { stream in
339
433
stream. write ( content)
340
434
}
435
+
436
+ if packageType == . macro {
437
+ try writeMacroPluginSources ( sources. appending ( " \( pkgname) Macros " ) )
438
+ try writeMacroClientSources ( sources. appending ( " \( pkgname) Client " ) )
439
+ }
341
440
}
342
441
343
442
private func writeTests( ) throws {
@@ -374,6 +473,113 @@ public final class InitPackage {
374
473
}
375
474
}
376
475
476
+ private func writeMacroTestsFile( _ path: AbsolutePath ) throws {
477
+ try writePackageFile ( path) { stream in
478
+ stream <<< ##"""
479
+ import SwiftSyntax
480
+ import SwiftSyntaxBuilder
481
+ import SwiftSyntaxMacros
482
+ import XCTest
483
+ import \##( moduleName) Macros
484
+
485
+ var testMacros: [String: Macro.Type] = [
486
+ "stringify" : StringifyMacro.self,
487
+ ]
488
+
489
+ final class \##( moduleName) Tests: XCTestCase {
490
+ func testMacro() {
491
+ // XCTest Documentation
492
+ // https://developer.apple.com/documentation/xctest
493
+
494
+ // Test input is a source file containing uses of the macro.
495
+ let sf: SourceFileSyntax =
496
+ #"""
497
+ let a = #stringify(x + y)
498
+ let b = #stringify("Hello, \(name)")
499
+ """#
500
+ let context = BasicMacroExpansionContext.init(
501
+ sourceFiles: [sf: .init(moduleName: "MyModule", fullFilePath: "test.swift")]
502
+ )
503
+
504
+ // Expand the macro to produce a new source file with the
505
+ // result of the expansion, and ensure that it has the
506
+ // expected source code.
507
+ let transformedSF = sf.expand(macros: testMacros, in: context)
508
+ XCTAssertEqual(
509
+ transformedSF.description,
510
+ #"""
511
+ let a = (x + y, "x + y")
512
+ let b = ("Hello, \(name)", #""Hello, \(name)""#)
513
+ """#
514
+ )
515
+ }
516
+ }
517
+
518
+ """##
519
+ }
520
+ }
521
+
522
+ private func writeMacroPluginSources( _ path: AbsolutePath ) throws {
523
+ try makeDirectories ( path)
524
+
525
+ try writePackageFile ( path. appending ( " \( moduleName) Macro.swift " ) ) { stream in
526
+ stream <<< ##"""
527
+ import SwiftCompilerPlugin
528
+ import SwiftSyntax
529
+ import SwiftSyntaxBuilder
530
+ import SwiftSyntaxMacros
531
+
532
+ /// Implementation of the `stringify` macro, which takes an expression
533
+ /// of any type and produces a tuple containing the value of that expression
534
+ /// and the source code that produced the value. For example
535
+ ///
536
+ /// #stringify(x + y)
537
+ ///
538
+ /// will expand to
539
+ ///
540
+ /// (x + y, "x + y")
541
+ public struct StringifyMacro: ExpressionMacro {
542
+ public static func expansion(
543
+ of node: some FreestandingMacroExpansionSyntax,
544
+ in context: some MacroExpansionContext
545
+ ) -> ExprSyntax {
546
+ guard let argument = node.argumentList.first?.expression else {
547
+ fatalError("compiler bug: the macro does not have any arguments")
548
+ }
549
+
550
+ return "(\(argument), \(literal: argument.description))"
551
+ }
552
+ }
553
+
554
+ @main
555
+ struct \##( moduleName) Plugin: CompilerPlugin {
556
+ let providingMacros: [Macro.Type] = [
557
+ StringifyMacro.self,
558
+ ]
559
+ }
560
+
561
+ """##
562
+ }
563
+ }
564
+
565
+ private func writeMacroClientSources( _ path: AbsolutePath ) throws {
566
+ try makeDirectories ( path)
567
+
568
+ try writePackageFile ( path. appending ( " main.swift " ) ) { stream in
569
+ stream <<< ##"""
570
+ import \##( moduleName)
571
+
572
+ let a = 17
573
+ let b = 25
574
+
575
+ let (result, code) = #stringify(a + b)
576
+
577
+ print("The value \(result) was produced by the code \"\(code)\"")
578
+
579
+ """##
580
+ }
581
+ }
582
+
377
583
private func writeTestFileStubs( testsPath: AbsolutePath ) throws {
378
584
let testModule = try AbsolutePath ( validating: pkgname + Target. testModuleNameSuffix, relativeTo: testsPath)
379
585
progressReporter ? ( " Creating \( testModule. relative ( to: destinationPath) ) / " )
@@ -384,6 +590,8 @@ public final class InitPackage {
384
590
case . empty, . `extension`, . executable, . tool: break
385
591
case . library:
386
592
try writeLibraryTestsFile ( testClassFile)
593
+ case . macro:
594
+ try writeMacroTestsFile ( testClassFile)
387
595
}
388
596
}
389
597
}
0 commit comments