Skip to content

Commit 39a4239

Browse files
authored
Merge pull request #2059 from Matejkob/incorporate-Dougs-macro-examples
Incorporate Doug’s macro examples in swift-syntax/Examples
2 parents f025614 + 5db504a commit 39a4239

25 files changed

+2362
-1
lines changed

Examples/Package.swift

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
// swift-tools-version: 5.7
1+
// swift-tools-version: 5.9
22

33
import PackageDescription
4+
import CompilerPluginSupport
45

56
let package = Package(
67
name: "Examples",
@@ -38,8 +39,49 @@ let package = Package(
3839
.product(name: "SwiftDiagnostics", package: "swift-syntax"),
3940
]
4041
),
42+
.macro(
43+
name: "MacroExamplesImplementation",
44+
dependencies: [
45+
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
46+
.product(name: "SwiftSyntax", package: "swift-syntax"),
47+
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
48+
.product(name: "SwiftDiagnostics", package: "swift-syntax"),
49+
],
50+
path: "Sources/MacroExamples/Implementation"
51+
),
52+
.testTarget(
53+
name: "MacroExamplesImplementationTests",
54+
dependencies: [
55+
"MacroExamplesImplementation"
56+
],
57+
path: "Tests/MacroExamples/Implementation"
58+
),
59+
]
60+
)
61+
62+
// The following targets are added only if the OS is macOS. This is because Swift macros are currently
63+
// not available on other platforms. As a result, we're guarding these targets with `#if os(macOS)`
64+
// to ensure that they are only included in the package when building on a macOS system.
65+
#if os(macOS)
66+
package.targets.append(
67+
contentsOf: [
68+
.target(
69+
name: "MacroExamplesInterface",
70+
dependencies: [
71+
"MacroExamplesImplementation"
72+
],
73+
path: "Sources/MacroExamples/Interface"
74+
),
75+
.executableTarget(
76+
name: "MacroExamplesPlayground",
77+
dependencies: [
78+
"MacroExamplesInterface"
79+
],
80+
path: "Sources/MacroExamples/Playground"
81+
),
4182
]
4283
)
84+
#endif
4385

4486
// This is a fake target that depends on all targets in the package.
4587
// We need to define it manually because the `Examples-Package` target doesn't exist for `swift build`.
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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+
import SwiftSyntax
14+
import SwiftSyntaxMacros
15+
16+
extension SyntaxCollection {
17+
mutating func removeLast() {
18+
self.remove(at: self.index(before: self.endIndex))
19+
}
20+
}
21+
22+
public struct AddAsyncMacro: PeerMacro {
23+
public static func expansion<
24+
Context: MacroExpansionContext,
25+
Declaration: DeclSyntaxProtocol
26+
>(
27+
of node: AttributeSyntax,
28+
providingPeersOf declaration: Declaration,
29+
in context: Context
30+
) throws -> [DeclSyntax] {
31+
32+
// Only on functions at the moment.
33+
guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
34+
throw CustomError.message("@addAsync only works on functions")
35+
}
36+
37+
// This only makes sense for non async functions.
38+
if funcDecl.signature.effectSpecifiers?.asyncSpecifier != nil {
39+
throw CustomError.message(
40+
"@addAsync requires an non async function"
41+
)
42+
}
43+
44+
// This only makes sense void functions
45+
if funcDecl.signature.returnClause?.type.with(\.leadingTrivia, []).with(\.trailingTrivia, []).description != "Void" {
46+
throw CustomError.message(
47+
"@addAsync requires an function that returns void"
48+
)
49+
}
50+
51+
// Requires a completion handler block as last parameter
52+
guard let completionHandlerParameterAttribute = funcDecl.signature.parameterClause.parameters.last?.type.as(AttributedTypeSyntax.self),
53+
let completionHandlerParameter = completionHandlerParameterAttribute.baseType.as(FunctionTypeSyntax.self)
54+
else {
55+
throw CustomError.message(
56+
"@addAsync requires an function that has a completion handler as last parameter"
57+
)
58+
}
59+
60+
// Completion handler needs to return Void
61+
if completionHandlerParameter.returnClause.type.with(\.leadingTrivia, []).with(\.trailingTrivia, []).description != "Void" {
62+
throw CustomError.message(
63+
"@addAsync requires an function that has a completion handler that returns Void"
64+
)
65+
}
66+
67+
let returnType = completionHandlerParameter.parameters.first?.type
68+
69+
let isResultReturn = returnType?.children(viewMode: .all).first?.description == "Result"
70+
let successReturnType = isResultReturn ? returnType!.as(IdentifierTypeSyntax.self)!.genericArgumentClause?.arguments.first!.argument : returnType
71+
72+
// Remove completionHandler and comma from the previous parameter
73+
var newParameterList = funcDecl.signature.parameterClause.parameters
74+
newParameterList.removeLast()
75+
let newParameterListLastParameter = newParameterList.last!
76+
newParameterList.removeLast()
77+
newParameterList.append(newParameterListLastParameter.with(\.trailingTrivia, []).with(\.trailingComma, nil))
78+
79+
// Drop the @addAsync attribute from the new declaration.
80+
let newAttributeList = funcDecl.attributes.filter {
81+
guard case let .attribute(attribute) = $0,
82+
let attributeType = attribute.attributeName.as(IdentifierTypeSyntax.self),
83+
let nodeType = node.attributeName.as(IdentifierTypeSyntax.self)
84+
else {
85+
return true
86+
}
87+
88+
return attributeType.name.text != nodeType.name.text
89+
}
90+
91+
let callArguments: [String] = newParameterList.map { param in
92+
let argName = param.secondName ?? param.firstName
93+
94+
let paramName = param.firstName
95+
if paramName.text != "_" {
96+
return "\(paramName.text): \(argName.text)"
97+
}
98+
99+
return "\(argName.text)"
100+
}
101+
102+
let switchBody: ExprSyntax =
103+
"""
104+
switch returnValue {
105+
case .success(let value):
106+
continuation.resume(returning: value)
107+
case .failure(let error):
108+
continuation.resume(throwing: error)
109+
}
110+
"""
111+
112+
let newBody: ExprSyntax =
113+
"""
114+
115+
\(raw: isResultReturn ? "try await withCheckedThrowingContinuation { continuation in" : "await withCheckedContinuation { continuation in")
116+
\(raw: funcDecl.name)(\(raw: callArguments.joined(separator: ", "))) { \(raw: returnType != nil ? "returnValue in" : "")
117+
118+
\(raw: isResultReturn ? switchBody : "continuation.resume(returning: \(raw: returnType != nil ? "returnValue" : "()"))")
119+
120+
}
121+
}
122+
123+
"""
124+
125+
let newFunc =
126+
funcDecl
127+
.with(
128+
\.signature,
129+
funcDecl.signature
130+
.with(
131+
\.effectSpecifiers,
132+
FunctionEffectSpecifiersSyntax(
133+
leadingTrivia: .space,
134+
asyncSpecifier: .keyword(.async),
135+
throwsSpecifier: isResultReturn ? .keyword(.throws) : nil
136+
) // add async
137+
)
138+
.with(
139+
\.returnClause,
140+
successReturnType != nil ? ReturnClauseSyntax(leadingTrivia: .space, type: successReturnType!.with(\.leadingTrivia, .space)) : nil
141+
) // add result type
142+
.with(
143+
\.parameterClause,
144+
funcDecl.signature.parameterClause.with(\.parameters, newParameterList) // drop completion handler
145+
.with(\.trailingTrivia, [])
146+
)
147+
)
148+
.with(
149+
\.body,
150+
CodeBlockSyntax(
151+
leftBrace: .leftBraceToken(leadingTrivia: .space),
152+
statements: CodeBlockItemListSyntax(
153+
[CodeBlockItemSyntax(item: .expr(newBody))]
154+
),
155+
rightBrace: .rightBraceToken(leadingTrivia: .newline)
156+
)
157+
)
158+
.with(\.attributes, newAttributeList)
159+
.with(\.leadingTrivia, .newlines(2))
160+
161+
return [DeclSyntax(newFunc)]
162+
}
163+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
import SwiftSyntaxMacros
14+
import SwiftSyntax
15+
import SwiftOperators
16+
import SwiftDiagnostics
17+
18+
/// Implementation of the `addBlocker` macro, which demonstrates how to
19+
/// produce detailed diagnostics from a macro implementation for an utterly
20+
/// silly task: warning about every "add" (binary +) in the argument, with a
21+
/// Fix-It that changes it to a "-".
22+
public struct AddBlocker: ExpressionMacro {
23+
class AddVisitor: SyntaxRewriter {
24+
var diagnostics: [Diagnostic] = []
25+
26+
override func visit(
27+
_ node: InfixOperatorExprSyntax
28+
) -> ExprSyntax {
29+
// Identify any infix operator + in the tree.
30+
if let binOp = node.operator.as(BinaryOperatorExprSyntax.self) {
31+
if binOp.operator.text == "+" {
32+
// Form the warning
33+
let messageID = MessageID(domain: "silly", id: "addblock")
34+
diagnostics.append(
35+
Diagnostic(
36+
// Where the warning should go (on the "+").
37+
node: Syntax(node.operator),
38+
// The warning message and severity.
39+
message: SimpleDiagnosticMessage(
40+
message: "blocked an add; did you mean to subtract?",
41+
diagnosticID: messageID,
42+
severity: .warning
43+
),
44+
// Highlight the left and right sides of the `+`.
45+
highlights: [
46+
Syntax(node.leftOperand),
47+
Syntax(node.rightOperand),
48+
],
49+
fixIts: [
50+
// Fix-It to replace the '+' with a '-'.
51+
FixIt(
52+
message: SimpleDiagnosticMessage(
53+
message: "use '-'",
54+
diagnosticID: messageID,
55+
severity: .error
56+
),
57+
changes: [
58+
FixIt.Change.replace(
59+
oldNode: Syntax(binOp.operator),
60+
newNode: Syntax(
61+
TokenSyntax(
62+
.binaryOperator("-"),
63+
leadingTrivia: binOp.operator.leadingTrivia,
64+
trailingTrivia: binOp.operator.trailingTrivia,
65+
presence: .present
66+
)
67+
)
68+
)
69+
]
70+
)
71+
]
72+
)
73+
)
74+
75+
return ExprSyntax(
76+
node.with(
77+
\.operator,
78+
ExprSyntax(
79+
binOp.with(
80+
\.operator,
81+
binOp.operator.with(\.tokenKind, .binaryOperator("-"))
82+
)
83+
)
84+
)
85+
)
86+
}
87+
}
88+
89+
return ExprSyntax(node)
90+
}
91+
}
92+
93+
public static func expansion(
94+
of node: some FreestandingMacroExpansionSyntax,
95+
in context: some MacroExpansionContext
96+
) throws -> ExprSyntax {
97+
let visitor = AddVisitor()
98+
let result = visitor.rewrite(Syntax(node))
99+
100+
for diag in visitor.diagnostics {
101+
context.diagnose(diag)
102+
}
103+
104+
return result.asProtocol(FreestandingMacroExpansionSyntax.self)!.argumentList.first!.expression
105+
}
106+
}

0 commit comments

Comments
 (0)