-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Add support for creating a macro package #6250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,6 +45,7 @@ public final class InitPackage { | |
case executable = "executable" | ||
case tool = "tool" | ||
case `extension` = "extension" | ||
case macro = "macro" | ||
|
||
public var description: String { | ||
return rawValue | ||
|
@@ -135,6 +136,16 @@ public final class InitPackage { | |
|
||
import PackageDescription | ||
|
||
""" | ||
|
||
if packageType == .macro { | ||
stream <<< """ | ||
import CompilerPluginSupport | ||
|
||
""" | ||
} | ||
|
||
stream <<< """ | ||
let package = Package( | ||
|
||
""" | ||
|
@@ -144,8 +155,29 @@ public final class InitPackage { | |
name: "\(pkgname)" | ||
""") | ||
|
||
var platforms = options.platforms | ||
|
||
// Macros require macOS 10.15, iOS 13, etc. | ||
if packageType == .macro { | ||
func addIfMissing(_ newPlatform: SupportedPlatform) { | ||
if platforms.contains(where: { platform in | ||
platform.platform == newPlatform.platform | ||
}) { | ||
return | ||
} | ||
|
||
platforms.append(newPlatform) | ||
} | ||
|
||
addIfMissing(.init(platform: .macOS, version: .init("10.15"))) | ||
addIfMissing(.init(platform: .iOS, version: .init("13"))) | ||
addIfMissing(.init(platform: .tvOS, version: .init("13"))) | ||
addIfMissing(.init(platform: .watchOS, version: .init("6"))) | ||
addIfMissing(.init(platform: .macCatalyst, version: .init("13"))) | ||
} | ||
|
||
var platformsParams = [String]() | ||
for supportedPlatform in options.platforms { | ||
for supportedPlatform in platforms { | ||
let version = supportedPlatform.version | ||
let platform = supportedPlatform.platform | ||
|
||
|
@@ -165,7 +197,7 @@ public final class InitPackage { | |
} | ||
|
||
// Package platforms | ||
if !options.platforms.isEmpty { | ||
if !platforms.isEmpty { | ||
pkgParams.append(""" | ||
platforms: [\(platformsParams.joined(separator: ", "))] | ||
""") | ||
|
@@ -181,6 +213,20 @@ public final class InitPackage { | |
targets: ["\(pkgname)"]), | ||
] | ||
""") | ||
} else if packageType == .macro { | ||
pkgParams.append(""" | ||
products: [ | ||
// Products define the executables and libraries a package produces, making them visible to other packages. | ||
.library( | ||
name: "\(pkgname)", | ||
targets: ["\(pkgname)"]), | ||
.executable( | ||
name: "\(pkgname)Client", | ||
targets: ["\(pkgname)Client"] | ||
), | ||
] | ||
""") | ||
|
||
} | ||
|
||
// Package dependencies | ||
|
@@ -190,6 +236,12 @@ public final class InitPackage { | |
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"), | ||
] | ||
""") | ||
} else if packageType == .macro { | ||
pkgParams.append(""" | ||
dependencies: [ | ||
.package(url: "https://github.com/apple/swift-syntax.git", branch: "main"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should make sure to have a follow-up task to change this from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right. We should get that once we branch for Swift 5.9. |
||
] | ||
""") | ||
} | ||
|
||
// Package targets | ||
|
@@ -219,6 +271,32 @@ public final class InitPackage { | |
path: "Sources"), | ||
] | ||
""" | ||
} else if packageType == .macro { | ||
param += """ | ||
// Macro implementation, only built for the host and never part of a client program. | ||
.macro(name: "\(pkgname)Macros", | ||
dependencies: [ | ||
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"), | ||
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"), | ||
neonichu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
] | ||
), | ||
|
||
// Library that exposes a macro as part of its API, which is used in client programs. | ||
.target(name: "\(pkgname)", dependencies: ["\(pkgname)Macros"]), | ||
|
||
// A client of the library, which is able to use the macro in its | ||
// own code. | ||
.executableTarget(name: "\(pkgname)Client", dependencies: ["\(pkgname)"]), | ||
|
||
// A test target used to develop the macro implementation. | ||
.testTarget( | ||
name: "\(pkgname)Tests", | ||
dependencies: [ | ||
"\(pkgname)Macros", | ||
] | ||
), | ||
] | ||
""" | ||
} else { | ||
param += """ | ||
.target( | ||
|
@@ -239,7 +317,8 @@ public final class InitPackage { | |
// Create a tools version with current version but with patch set to zero. | ||
// We do this to avoid adding unnecessary constraints to patch versions, if | ||
// the package really needs it, they should add it manually. | ||
let version = InitPackage.newPackageToolsVersion.zeroedPatch | ||
let version = packageType == .macro ? ToolsVersion.vNext | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also something to keep in mind to update later. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, we'll update this when the |
||
: InitPackage.newPackageToolsVersion.zeroedPatch | ||
|
||
// Write the current tools version. | ||
try ToolsVersionSpecificationWriter.rewriteSpecification( | ||
|
@@ -331,13 +410,33 @@ public final class InitPackage { | |
} | ||
} | ||
""" | ||
case .macro: | ||
content = """ | ||
// The Swift Programming Language | ||
// https://docs.swift.org/swift-book | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe a more concrete reference to macros once we have it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, makes sense |
||
|
||
// A macro that produces both a value and a string containing the | ||
// source code that generated the value. For example, | ||
// | ||
// #stringify(x + y) | ||
// | ||
// produces a tuple `(x + y, "x + y")`. | ||
@freestanding(expression) | ||
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "\(moduleName)Macros", type: "StringifyMacro") | ||
""" | ||
|
||
case .empty, .`extension`: | ||
throw InternalError("invalid packageType \(packageType)") | ||
} | ||
|
||
try writePackageFile(sourceFile) { stream in | ||
stream.write(content) | ||
} | ||
|
||
if packageType == .macro { | ||
try writeMacroPluginSources(sources.appending("\(pkgname)Macros")) | ||
try writeMacroClientSources(sources.appending("\(pkgname)Client")) | ||
} | ||
} | ||
|
||
private func writeTests() throws { | ||
|
@@ -374,6 +473,113 @@ public final class InitPackage { | |
} | ||
} | ||
|
||
private func writeMacroTestsFile(_ path: AbsolutePath) throws { | ||
try writePackageFile(path) { stream in | ||
stream <<< ##""" | ||
import SwiftSyntax | ||
import SwiftSyntaxBuilder | ||
import SwiftSyntaxMacros | ||
import XCTest | ||
import \##(moduleName)Macros | ||
|
||
var testMacros: [String: Macro.Type] = [ | ||
"stringify" : StringifyMacro.self, | ||
] | ||
|
||
final class \##(moduleName)Tests: XCTestCase { | ||
func testMacro() { | ||
// XCTest Documentation | ||
// https://developer.apple.com/documentation/xctest | ||
|
||
// Test input is a source file containing uses of the macro. | ||
let sf: SourceFileSyntax = | ||
#""" | ||
let a = #stringify(x + y) | ||
let b = #stringify("Hello, \(name)") | ||
"""# | ||
let context = BasicMacroExpansionContext.init( | ||
sourceFiles: [sf: .init(moduleName: "MyModule", fullFilePath: "test.swift")] | ||
) | ||
|
||
// Expand the macro to produce a new source file with the | ||
// result of the expansion, and ensure that it has the | ||
// expected source code. | ||
let transformedSF = sf.expand(macros: testMacros, in: context) | ||
XCTAssertEqual( | ||
transformedSF.description, | ||
#""" | ||
let a = (x + y, "x + y") | ||
let b = ("Hello, \(name)", #""Hello, \(name)""#) | ||
"""# | ||
) | ||
} | ||
} | ||
|
||
"""## | ||
} | ||
} | ||
|
||
private func writeMacroPluginSources(_ path: AbsolutePath) throws { | ||
try makeDirectories(path) | ||
|
||
try writePackageFile(path.appending("\(moduleName)Macro.swift")) { stream in | ||
stream <<< ##""" | ||
import SwiftCompilerPlugin | ||
import SwiftSyntax | ||
import SwiftSyntaxBuilder | ||
import SwiftSyntaxMacros | ||
|
||
/// Implementation of the `stringify` macro, which takes an expression | ||
/// of any type and produces a tuple containing the value of that expression | ||
/// and the source code that produced the value. For example | ||
/// | ||
/// #stringify(x + y) | ||
/// | ||
/// will expand to | ||
/// | ||
/// (x + y, "x + y") | ||
public struct StringifyMacro: ExpressionMacro { | ||
public static func expansion( | ||
of node: some FreestandingMacroExpansionSyntax, | ||
in context: some MacroExpansionContext | ||
) -> ExprSyntax { | ||
guard let argument = node.argumentList.first?.expression else { | ||
fatalError("compiler bug: the macro does not have any arguments") | ||
} | ||
|
||
return "(\(argument), \(literal: argument.description))" | ||
} | ||
} | ||
|
||
@main | ||
struct \##(moduleName)Plugin: CompilerPlugin { | ||
let providingMacros: [Macro.Type] = [ | ||
StringifyMacro.self, | ||
] | ||
} | ||
|
||
"""## | ||
} | ||
} | ||
|
||
private func writeMacroClientSources(_ path: AbsolutePath) throws { | ||
try makeDirectories(path) | ||
|
||
try writePackageFile(path.appending("main.swift")) { stream in | ||
stream <<< ##""" | ||
import \##(moduleName) | ||
|
||
let a = 17 | ||
let b = 25 | ||
|
||
let (result, code) = #stringify(a + b) | ||
|
||
print("The value \(result) was produced by the code \"\(code)\"") | ||
|
||
"""## | ||
} | ||
} | ||
|
||
private func writeTestFileStubs(testsPath: AbsolutePath) throws { | ||
let testModule = try AbsolutePath(validating: pkgname + Target.testModuleNameSuffix, relativeTo: testsPath) | ||
progressReporter?("Creating \(testModule.relative(to: destinationPath))/") | ||
|
@@ -384,6 +590,8 @@ public final class InitPackage { | |
case .empty, .`extension`, .executable, .tool: break | ||
case .library: | ||
try writeLibraryTestsFile(testClassFile) | ||
case .macro: | ||
try writeMacroTestsFile(testClassFile) | ||
} | ||
} | ||
} | ||
|
Uh oh!
There was an error while loading. Please reload this page.