mirror of
https://github.com/apple/swift.git
synced 2025-12-21 12:14:44 +01:00
Provide ASTWalker with a customization point to specify whether to check macro arguments (which are type checked but never emitted), the macro expansion (which is the result of applying the macro and is actually emitted into the source), or both. Provide answers for the ~115 different ASTWalker visitors throughout the code base. Fixes rdar://104042945, which concerns checking of effects in macro arguments---which we shouldn't do.
1074 lines
29 KiB
Swift
1074 lines
29 KiB
Swift
import SwiftDiagnostics
|
|
import SwiftOperators
|
|
import SwiftSyntax
|
|
import SwiftSyntaxBuilder
|
|
import SwiftSyntaxMacros
|
|
|
|
/// Replace the label of the first element in the tuple with the given
|
|
/// new label.
|
|
private func replaceFirstLabel(
|
|
of tuple: TupleExprElementListSyntax, with newLabel: String
|
|
) -> TupleExprElementListSyntax{
|
|
guard let firstElement = tuple.first else {
|
|
return tuple
|
|
}
|
|
|
|
return tuple.replacing(
|
|
childAt: 0, with: firstElement.with(\.label, .identifier(newLabel)))
|
|
}
|
|
|
|
public struct ColorLiteralMacro: ExpressionMacro {
|
|
public static func expansion(
|
|
of macro: some FreestandingMacroExpansionSyntax,
|
|
in context: some MacroExpansionContext
|
|
) -> ExprSyntax {
|
|
let argList = replaceFirstLabel(
|
|
of: macro.argumentList, with: "_colorLiteralRed"
|
|
)
|
|
let initSyntax: ExprSyntax = ".init(\(argList))"
|
|
if let leadingTrivia = macro.leadingTrivia {
|
|
return initSyntax.with(\.leadingTrivia, leadingTrivia)
|
|
}
|
|
return initSyntax
|
|
}
|
|
}
|
|
|
|
public struct FileIDMacro: ExpressionMacro {
|
|
public static func expansion<
|
|
Node: FreestandingMacroExpansionSyntax,
|
|
Context: MacroExpansionContext
|
|
>(
|
|
of macro: Node,
|
|
in context: Context
|
|
) throws -> ExprSyntax {
|
|
guard let sourceLoc = context.location(of: macro),
|
|
let fileID = sourceLoc.file
|
|
else {
|
|
throw CustomError.message("can't find location for macro")
|
|
}
|
|
|
|
let fileLiteral: ExprSyntax = "\(literal: fileID)"
|
|
if let leadingTrivia = macro.leadingTrivia {
|
|
return fileLiteral.with(\.leadingTrivia, leadingTrivia)
|
|
}
|
|
return fileLiteral
|
|
}
|
|
}
|
|
|
|
public struct StringifyMacro: ExpressionMacro {
|
|
public static func expansion(
|
|
of macro: some FreestandingMacroExpansionSyntax,
|
|
in context: some MacroExpansionContext
|
|
) -> ExprSyntax {
|
|
guard let argument = macro.argumentList.first?.expression else {
|
|
fatalError("boom")
|
|
}
|
|
|
|
return "(\(argument), \(StringLiteralExprSyntax(content: argument.description)))"
|
|
}
|
|
}
|
|
|
|
public struct StringifyAndTryMacro: ExpressionMacro {
|
|
public static func expansion(
|
|
of macro: some FreestandingMacroExpansionSyntax,
|
|
in context: some MacroExpansionContext
|
|
) -> ExprSyntax {
|
|
guard let argument = macro.argumentList.first?.expression else {
|
|
fatalError("boom")
|
|
}
|
|
|
|
return "(try \(argument), \(StringLiteralExprSyntax(content: argument.description)))"
|
|
}
|
|
}
|
|
|
|
struct SimpleDiagnosticMessage: DiagnosticMessage {
|
|
let message: String
|
|
let diagnosticID: MessageID
|
|
let severity: DiagnosticSeverity
|
|
}
|
|
|
|
extension SimpleDiagnosticMessage: FixItMessage {
|
|
var fixItID: MessageID { diagnosticID }
|
|
}
|
|
|
|
public enum AddBlocker: ExpressionMacro {
|
|
class AddVisitor: SyntaxRewriter {
|
|
var diagnostics: [Diagnostic] = []
|
|
|
|
override func visit(
|
|
_ node: InfixOperatorExprSyntax
|
|
) -> ExprSyntax {
|
|
if let binOp = node.operatorOperand.as(BinaryOperatorExprSyntax.self) {
|
|
if binOp.operatorToken.text == "+" {
|
|
let messageID = MessageID(domain: "silly", id: "addblock")
|
|
diagnostics.append(
|
|
Diagnostic(
|
|
node: Syntax(node.operatorOperand),
|
|
message: SimpleDiagnosticMessage(
|
|
message: "blocked an add; did you mean to subtract?",
|
|
diagnosticID: messageID,
|
|
severity: .error
|
|
),
|
|
highlights: [
|
|
Syntax(node.leftOperand),
|
|
Syntax(node.rightOperand)
|
|
],
|
|
fixIts: [
|
|
FixIt(
|
|
message: SimpleDiagnosticMessage(
|
|
message: "use '-'",
|
|
diagnosticID: messageID,
|
|
severity: .error
|
|
),
|
|
changes: [
|
|
FixIt.Change.replace(
|
|
oldNode: Syntax(binOp.operatorToken),
|
|
newNode: Syntax(
|
|
TokenSyntax(
|
|
.binaryOperator("-"),
|
|
leadingTrivia: binOp.operatorToken.leadingTrivia,
|
|
trailingTrivia: binOp.operatorToken.trailingTrivia,
|
|
presence: .present
|
|
)
|
|
)
|
|
)
|
|
]
|
|
),
|
|
]
|
|
)
|
|
)
|
|
|
|
return ExprSyntax(
|
|
node.with(
|
|
\.operatorOperand,
|
|
ExprSyntax(
|
|
binOp.with(
|
|
\.operatorToken,
|
|
binOp.operatorToken.withKind(.binaryOperator("-"))
|
|
)
|
|
)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
return ExprSyntax(node)
|
|
}
|
|
}
|
|
|
|
public static func expansion(
|
|
of node: some FreestandingMacroExpansionSyntax,
|
|
in context: some MacroExpansionContext
|
|
) -> ExprSyntax {
|
|
let visitor = AddVisitor()
|
|
let result = visitor.visit(Syntax(node))
|
|
|
|
for diag in visitor.diagnostics {
|
|
context.diagnose(diag)
|
|
}
|
|
|
|
return result.asProtocol(FreestandingMacroExpansionSyntax.self)!.argumentList.first!.expression
|
|
}
|
|
}
|
|
|
|
public class RecursiveMacro: ExpressionMacro {
|
|
public static func expansion(
|
|
of macro: some FreestandingMacroExpansionSyntax,
|
|
in context: some MacroExpansionContext
|
|
) -> ExprSyntax {
|
|
guard let argument = macro.argumentList.first?.expression,
|
|
argument.description == "false" else {
|
|
return "\(macro)"
|
|
}
|
|
|
|
return "()"
|
|
}
|
|
}
|
|
|
|
public class NestedDeclInExprMacro: ExpressionMacro {
|
|
public static func expansion(
|
|
of macro: some FreestandingMacroExpansionSyntax,
|
|
in context: some MacroExpansionContext
|
|
) -> ExprSyntax {
|
|
return """
|
|
{ () -> Void in
|
|
struct Foo { }
|
|
return ()
|
|
}
|
|
"""
|
|
}
|
|
}
|
|
|
|
enum CustomError: Error, CustomStringConvertible {
|
|
case message(String)
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .message(let text):
|
|
return text
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct DefineBitwidthNumberedStructsMacro: DeclarationMacro {
|
|
public static func expansion(
|
|
of node: some FreestandingMacroExpansionSyntax,
|
|
in context: some MacroExpansionContext
|
|
) throws -> [DeclSyntax] {
|
|
guard let firstElement = node.argumentList.first,
|
|
let stringLiteral = firstElement.expression.as(StringLiteralExprSyntax.self),
|
|
stringLiteral.segments.count == 1,
|
|
case let .stringSegment(prefix) = stringLiteral.segments.first else {
|
|
throw CustomError.message("#bitwidthNumberedStructs macro requires a string literal")
|
|
}
|
|
|
|
if prefix.content.text == "BUG" {
|
|
return [
|
|
"""
|
|
|
|
struct \(raw: prefix) {
|
|
func \(context.createUniqueName("method"))() { return 0 }
|
|
func \(context.createUniqueName("method"))() { return 1 }
|
|
}
|
|
"""
|
|
]
|
|
}
|
|
|
|
return [8, 16, 32, 64].map { bitwidth in
|
|
"""
|
|
|
|
struct \(raw: prefix)\(raw: String(bitwidth)) {
|
|
func \(context.createUniqueName("method"))() { }
|
|
func \(context.createUniqueName("method"))() { }
|
|
}
|
|
"""
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct DefineDeclsWithKnownNamesMacro: DeclarationMacro {
|
|
public static func expansion(
|
|
of node: some FreestandingMacroExpansionSyntax,
|
|
in context: some MacroExpansionContext
|
|
) throws -> [DeclSyntax] {
|
|
return [
|
|
"""
|
|
|
|
struct A {
|
|
func \(context.createUniqueName("method"))() { }
|
|
func \(context.createUniqueName("method"))() { }
|
|
}
|
|
""",
|
|
"""
|
|
|
|
struct B {
|
|
func \(context.createUniqueName("method"))() { }
|
|
func \(context.createUniqueName("method"))() { }
|
|
}
|
|
""",
|
|
"""
|
|
|
|
var foo: Int {
|
|
1
|
|
}
|
|
""",
|
|
"""
|
|
|
|
var addOne: (Int) -> Int { { $0 + 1 } }
|
|
"""
|
|
|
|
// FIXME:
|
|
// 1. Stored properties are not visited in IRGen
|
|
// let addTwo: (Int) -> Int = { $0 + 2 }
|
|
// 2. Curry thunk at call sites
|
|
// func foo()
|
|
// Foo2.foo // AutoClosureExpr with invalid discriminator
|
|
]
|
|
}
|
|
}
|
|
|
|
public struct WarningMacro: ExpressionMacro {
|
|
public static func expansion(
|
|
of macro: some FreestandingMacroExpansionSyntax,
|
|
in context: some MacroExpansionContext
|
|
) throws -> ExprSyntax {
|
|
guard let firstElement = macro.argumentList.first,
|
|
let stringLiteral = firstElement.expression
|
|
.as(StringLiteralExprSyntax.self),
|
|
stringLiteral.segments.count == 1,
|
|
case let .stringSegment(messageString)? = stringLiteral.segments.first
|
|
else {
|
|
throw CustomError.message("#myWarning macro requires a string literal")
|
|
}
|
|
|
|
context.diagnose(
|
|
Diagnostic(
|
|
node: Syntax(macro),
|
|
message: SimpleDiagnosticMessage(
|
|
message: messageString.content.description,
|
|
diagnosticID: MessageID(domain: "test", id: "error"),
|
|
severity: .warning
|
|
)
|
|
)
|
|
)
|
|
|
|
return "()"
|
|
}
|
|
}
|
|
|
|
public struct PropertyWrapperMacro {}
|
|
|
|
extension PropertyWrapperMacro: AccessorMacro, Macro {
|
|
public static func expansion(
|
|
of node: AttributeSyntax,
|
|
providingAccessorsOf declaration: some DeclSyntaxProtocol,
|
|
in context: some MacroExpansionContext
|
|
) throws -> [AccessorDeclSyntax] {
|
|
guard let varDecl = declaration.as(VariableDeclSyntax.self),
|
|
let binding = varDecl.bindings.first,
|
|
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier,
|
|
binding.accessor == nil
|
|
else {
|
|
return []
|
|
}
|
|
|
|
return [
|
|
"""
|
|
|
|
get {
|
|
_\(identifier).wrappedValue
|
|
}
|
|
""",
|
|
"""
|
|
|
|
set {
|
|
_\(identifier).wrappedValue = newValue
|
|
}
|
|
""",
|
|
]
|
|
}
|
|
}
|
|
|
|
public struct WrapAllProperties: MemberAttributeMacro {
|
|
public static func expansion(
|
|
of node: AttributeSyntax,
|
|
attachedTo parent: some DeclGroupSyntax,
|
|
providingAttributesFor member: some DeclSyntaxProtocol,
|
|
in context: some MacroExpansionContext
|
|
) throws -> [AttributeSyntax] {
|
|
guard member.is(VariableDeclSyntax.self) else {
|
|
return []
|
|
}
|
|
|
|
let wrapperTypeName: String
|
|
if parent.is(ClassDeclSyntax.self) {
|
|
wrapperTypeName = "EnclosingSelfWrapper"
|
|
} else {
|
|
wrapperTypeName = "Wrapper"
|
|
}
|
|
|
|
let propertyWrapperAttr = AttributeSyntax(
|
|
attributeName: SimpleTypeIdentifierSyntax(
|
|
name: .identifier(wrapperTypeName)
|
|
)
|
|
)
|
|
|
|
return [propertyWrapperAttr]
|
|
}
|
|
}
|
|
|
|
public struct TypeWrapperMacro {}
|
|
|
|
extension TypeWrapperMacro: MemberAttributeMacro {
|
|
public static func expansion(
|
|
of node: AttributeSyntax,
|
|
attachedTo decl: some DeclGroupSyntax,
|
|
providingAttributesFor member: some DeclSyntaxProtocol,
|
|
in context: some MacroExpansionContext
|
|
) throws -> [AttributeSyntax] {
|
|
guard let varDecl = member.as(VariableDeclSyntax.self),
|
|
let binding = varDecl.bindings.first,
|
|
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier,
|
|
binding.accessor == nil
|
|
else {
|
|
return []
|
|
}
|
|
|
|
if identifier.text == "_storage" {
|
|
return []
|
|
}
|
|
|
|
let customAttr = AttributeSyntax(
|
|
attributeName: SimpleTypeIdentifierSyntax(
|
|
name: .identifier("accessViaStorage")
|
|
)
|
|
)
|
|
|
|
return [customAttr]
|
|
}
|
|
}
|
|
|
|
extension TypeWrapperMacro: MemberMacro {
|
|
public static func expansion(
|
|
of node: AttributeSyntax,
|
|
providingMembersOf decl: some DeclGroupSyntax,
|
|
in context: some MacroExpansionContext
|
|
) throws -> [DeclSyntax] {
|
|
let storageVariable: DeclSyntax =
|
|
"""
|
|
private var _storage = _Storage()
|
|
"""
|
|
|
|
return [
|
|
storageVariable,
|
|
]
|
|
}
|
|
}
|
|
|
|
public struct AccessViaStorageMacro: AccessorMacro {
|
|
public static func expansion(
|
|
of node: AttributeSyntax,
|
|
providingAccessorsOf declaration: some DeclSyntaxProtocol,
|
|
in context: some MacroExpansionContext
|
|
) throws -> [AccessorDeclSyntax] {
|
|
guard let varDecl = declaration.as(VariableDeclSyntax.self),
|
|
let binding = varDecl.bindings.first,
|
|
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier,
|
|
binding.accessor == nil
|
|
else {
|
|
return []
|
|
}
|
|
|
|
if identifier.text == "_storage" {
|
|
return []
|
|
}
|
|
|
|
return [
|
|
"get { _storage.\(identifier) }",
|
|
"set { _storage.\(identifier) = newValue }",
|
|
]
|
|
}
|
|
}
|
|
|
|
public struct AddMembers: MemberMacro {
|
|
public static func expansion(
|
|
of node: AttributeSyntax,
|
|
providingMembersOf decl: some DeclGroupSyntax,
|
|
in context: some MacroExpansionContext
|
|
) throws -> [DeclSyntax] {
|
|
let storageStruct: DeclSyntax =
|
|
"""
|
|
struct Storage {}
|
|
"""
|
|
|
|
let storageVariable: DeclSyntax =
|
|
"""
|
|
private var storage = Storage()
|
|
"""
|
|
|
|
let instanceMethod: DeclSyntax =
|
|
"""
|
|
func getStorage() -> Storage {
|
|
print("synthesized method")
|
|
return storage
|
|
}
|
|
"""
|
|
|
|
let staticMethod: DeclSyntax =
|
|
"""
|
|
static func method() {}
|
|
"""
|
|
|
|
let initDecl: DeclSyntax =
|
|
"""
|
|
init() {}
|
|
"""
|
|
|
|
return [
|
|
storageStruct,
|
|
storageVariable,
|
|
instanceMethod,
|
|
staticMethod,
|
|
initDecl,
|
|
]
|
|
}
|
|
}
|
|
|
|
/// Implementation of the `wrapStoredProperties` macro, which can be
|
|
/// used to apply an attribute to all of the stored properties of a type.
|
|
///
|
|
/// This macro demonstrates member-attribute macros.
|
|
public struct WrapStoredPropertiesMacro: MemberAttributeMacro {
|
|
public static func expansion<
|
|
Declaration: DeclGroupSyntax,
|
|
Context: MacroExpansionContext
|
|
>(
|
|
of node: AttributeSyntax,
|
|
attachedTo decl: Declaration,
|
|
providingAttributesFor member: some DeclSyntaxProtocol,
|
|
in context: Context
|
|
) throws -> [AttributeSyntax] {
|
|
guard let property = member.as(VariableDeclSyntax.self),
|
|
property.isStoredProperty
|
|
else {
|
|
return []
|
|
}
|
|
|
|
guard case let .argumentList(arguments) = node.argument,
|
|
let firstElement = arguments.first,
|
|
let stringLiteral = firstElement.expression
|
|
.as(StringLiteralExprSyntax.self),
|
|
stringLiteral.segments.count == 1,
|
|
case let .stringSegment(wrapperName)? = stringLiteral.segments.first else {
|
|
throw CustomError.message("macro requires a string literal containing the name of an attribute")
|
|
}
|
|
|
|
return [
|
|
AttributeSyntax(
|
|
attributeName: SimpleTypeIdentifierSyntax(
|
|
name: .identifier(wrapperName.content.text)
|
|
)
|
|
)
|
|
.with(\.leadingTrivia, [.newlines(1), .spaces(2)])
|
|
]
|
|
}
|
|
}
|
|
|
|
extension VariableDeclSyntax {
|
|
/// Determine whether this variable has the syntax of a stored property.
|
|
///
|
|
/// This syntactic check cannot account for semantic adjustments due to,
|
|
/// e.g., accessor macros or property wrappers.
|
|
var isStoredProperty: Bool {
|
|
if bindings.count != 1 {
|
|
return false
|
|
}
|
|
|
|
let binding = bindings.first!
|
|
switch binding.accessor {
|
|
case .none:
|
|
return true
|
|
|
|
case .accessors(let node):
|
|
for accessor in node.accessors {
|
|
switch accessor.accessorKind.tokenKind {
|
|
case .keyword(.willSet), .keyword(.didSet):
|
|
// Observers can occur on a stored property.
|
|
break
|
|
|
|
default:
|
|
// Other accessors make it a computed property.
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
|
|
case .getter:
|
|
return false
|
|
|
|
@unknown default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
extension DeclGroupSyntax {
|
|
/// Enumerate the stored properties that syntactically occur in this
|
|
/// declaration.
|
|
func storedProperties() -> [VariableDeclSyntax] {
|
|
return members.members.compactMap { member in
|
|
guard let variable = member.decl.as(VariableDeclSyntax.self),
|
|
variable.isStoredProperty else {
|
|
return nil
|
|
}
|
|
|
|
return variable
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum LeftHandOperandFinderMacro: ExpressionMacro {
|
|
class Visitor<Context: MacroExpansionContext>: SyntaxVisitor {
|
|
let context: Context
|
|
|
|
init(context: Context) {
|
|
self.context = context
|
|
super.init(viewMode: .sourceAccurate)
|
|
}
|
|
|
|
override func visit(
|
|
_ node: InfixOperatorExprSyntax
|
|
) -> SyntaxVisitorContinueKind {
|
|
guard let lhsStartLoc = context.location(of: node.leftOperand),
|
|
let lhsEndLoc = context.location(
|
|
of: node.leftOperand, at: .beforeTrailingTrivia, filePathMode: .fileID
|
|
) else {
|
|
fatalError("missing source location information")
|
|
}
|
|
|
|
print("Source range for LHS is \(lhsStartLoc.file!): \(lhsStartLoc.line!):\(lhsStartLoc.column!)-\(lhsEndLoc.line!):\(lhsEndLoc.column!)")
|
|
|
|
return .visitChildren
|
|
}
|
|
}
|
|
|
|
public static func expansion(
|
|
of node: some FreestandingMacroExpansionSyntax,
|
|
in context: some MacroExpansionContext
|
|
) -> ExprSyntax {
|
|
let visitor = Visitor(context: context)
|
|
visitor.walk(node)
|
|
|
|
return node.argumentList.first!.expression
|
|
}
|
|
}
|
|
|
|
public struct AddCompletionHandler: PeerMacro {
|
|
public static func expansion(
|
|
of node: AttributeSyntax,
|
|
providingPeersOf declaration: some DeclSyntaxProtocol,
|
|
in context: some MacroExpansionContext
|
|
) throws -> [DeclSyntax] {
|
|
// Only on functions at the moment. We could handle initializers as well
|
|
// with a bit of work.
|
|
guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
|
|
throw CustomError.message("@addCompletionHandler only works on functions")
|
|
}
|
|
|
|
// This only makes sense for async functions.
|
|
if funcDecl.signature.effectSpecifiers?.asyncSpecifier == nil {
|
|
throw CustomError.message(
|
|
"@addCompletionHandler requires an async function"
|
|
)
|
|
}
|
|
|
|
// Form the completion handler parameter.
|
|
let resultType: TypeSyntax? = funcDecl.signature.output?.returnType.with(\.leadingTrivia, []).with(\.trailingTrivia, [])
|
|
|
|
let completionHandlerParam =
|
|
FunctionParameterSyntax(
|
|
firstName: .identifier("completionHandler"),
|
|
colon: .colonToken(trailingTrivia: .space),
|
|
type: "@escaping (\(resultType ?? "")) -> Void" as TypeSyntax
|
|
)
|
|
|
|
// Add the completion handler parameter to the parameter list.
|
|
let parameterList = funcDecl.signature.input.parameterList
|
|
let newParameterList: FunctionParameterListSyntax
|
|
if let lastParam = parameterList.last {
|
|
// We need to add a trailing comma to the preceding list.
|
|
newParameterList = parameterList.removingLast()
|
|
.appending(
|
|
lastParam.with(
|
|
\.trailingComma,
|
|
.commaToken(trailingTrivia: .space)
|
|
)
|
|
)
|
|
.appending(completionHandlerParam)
|
|
} else {
|
|
newParameterList = parameterList.appending(completionHandlerParam)
|
|
}
|
|
|
|
let callArguments: [String] = try parameterList.map { param in
|
|
guard let argName = param.secondName ?? param.firstName else {
|
|
throw CustomError.message(
|
|
"@addCompletionHandler argument must have a name"
|
|
)
|
|
}
|
|
|
|
if let paramName = param.firstName, paramName.text != "_" {
|
|
return "\(paramName.text): \(argName.text)"
|
|
}
|
|
|
|
return "\(argName.text)"
|
|
}
|
|
|
|
let call: ExprSyntax =
|
|
"\(funcDecl.identifier)(\(raw: callArguments.joined(separator: ", ")))"
|
|
|
|
// FIXME: We should make CodeBlockSyntax ExpressibleByStringInterpolation,
|
|
// so that the full body could go here.
|
|
let newBody: ExprSyntax =
|
|
"""
|
|
|
|
Task {
|
|
completionHandler(await \(call))
|
|
}
|
|
|
|
"""
|
|
|
|
// Drop the @addCompletionHandler attribute from the new declaration.
|
|
let newAttributeList = AttributeListSyntax(
|
|
funcDecl.attributes?.filter {
|
|
guard case let .attribute(attribute) = $0,
|
|
let attributeType = attribute.attributeName.as(SimpleTypeIdentifierSyntax.self),
|
|
let nodeType = node.attributeName.as(SimpleTypeIdentifierSyntax.self)
|
|
else {
|
|
return true
|
|
}
|
|
|
|
return attributeType.name.text != nodeType.name.text
|
|
} ?? []
|
|
)
|
|
|
|
let newFunc =
|
|
funcDecl
|
|
.with(
|
|
\.signature,
|
|
funcDecl.signature
|
|
.with(
|
|
\.effectSpecifiers,
|
|
funcDecl.signature.effectSpecifiers?.with(\.asyncSpecifier, nil) // drop async
|
|
)
|
|
.with(\.output, nil) // drop result type
|
|
.with(
|
|
\.input, // add completion handler parameter
|
|
funcDecl.signature.input.with(\.parameterList, newParameterList)
|
|
.with(\.trailingTrivia, [])
|
|
)
|
|
)
|
|
.with(
|
|
\.body,
|
|
CodeBlockSyntax(
|
|
leftBrace: .leftBraceToken(leadingTrivia: .space),
|
|
statements: CodeBlockItemListSyntax(
|
|
[CodeBlockItemSyntax(item: .expr(newBody))]
|
|
),
|
|
rightBrace: .rightBraceToken(leadingTrivia: .newline)
|
|
)
|
|
)
|
|
.with(\.attributes, newAttributeList)
|
|
.with(\.leadingTrivia, .newlines(2))
|
|
|
|
return [DeclSyntax(newFunc)]
|
|
}
|
|
}
|
|
|
|
public struct WrapInType: PeerMacro {
|
|
public static func expansion(
|
|
of node: AttributeSyntax,
|
|
providingPeersOf declaration: some DeclSyntaxProtocol,
|
|
in context: some MacroExpansionContext
|
|
) throws -> [DeclSyntax] {
|
|
guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
|
|
throw CustomError.message("@wrapInType only applies to functions")
|
|
}
|
|
|
|
// Build a new function with the same signature that forwards arguments
|
|
// to the the original function.
|
|
let parameterList = funcDecl.signature.input.parameterList
|
|
let callArguments: [String] = try parameterList.map { param in
|
|
guard let argName = param.secondName ?? param.firstName else {
|
|
throw CustomError.message("@wrapInType argument must have a name")
|
|
}
|
|
|
|
if let paramName = param.firstName, paramName.text != "_" {
|
|
return "\(paramName.text): \(argName.text)"
|
|
}
|
|
|
|
return "\(argName.text)"
|
|
}
|
|
|
|
let call: ExprSyntax =
|
|
"""
|
|
\(funcDecl.identifier)(\(raw: callArguments.joined(separator: ", ")))
|
|
"""
|
|
|
|
// Drop the peer macro attribute from the new declaration.
|
|
let newAttributeList = AttributeListSyntax(
|
|
funcDecl.attributes?.filter {
|
|
guard case let .attribute(attribute) = $0,
|
|
let attributeType = attribute.attributeName.as(SimpleTypeIdentifierSyntax.self),
|
|
let nodeType = node.attributeName.as(SimpleTypeIdentifierSyntax.self)
|
|
else {
|
|
return true
|
|
}
|
|
|
|
return attributeType.name.text != nodeType.name.text
|
|
} ?? []
|
|
)
|
|
|
|
let method =
|
|
funcDecl
|
|
.with(
|
|
\.identifier,
|
|
"\(context.createUniqueName(funcDecl.identifier.text))"
|
|
)
|
|
.with(
|
|
\.signature,
|
|
funcDecl.signature
|
|
)
|
|
.with(
|
|
\.body,
|
|
CodeBlockSyntax(
|
|
leftBrace: .leftBraceToken(leadingTrivia: .space),
|
|
statements: CodeBlockItemListSyntax(
|
|
[CodeBlockItemSyntax(item: .expr(call))]
|
|
)
|
|
.with(\.leadingTrivia, [.newlines(1), .spaces(2)]),
|
|
rightBrace: .rightBraceToken(leadingTrivia: .newline)
|
|
)
|
|
)
|
|
.with(\.attributes, newAttributeList)
|
|
|
|
let structType: DeclSyntax =
|
|
"""
|
|
struct \(context.createUniqueName(funcDecl.identifier.text)) {
|
|
\(method)
|
|
}
|
|
"""
|
|
|
|
return [structType]
|
|
}
|
|
}
|
|
|
|
private extension DeclSyntaxProtocol {
|
|
var isObservableStoredProperty: Bool {
|
|
if let property = self.as(VariableDeclSyntax.self),
|
|
let binding = property.bindings.first,
|
|
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier,
|
|
identifier.text != "_registrar", identifier.text != "_storage",
|
|
binding.accessor == nil {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|
|
|
|
public struct ObservableMacro: MemberMacro, MemberAttributeMacro {
|
|
|
|
// MARK: - MemberMacro
|
|
|
|
public static func expansion(
|
|
of node: AttributeSyntax,
|
|
providingMembersOf declaration: some DeclGroupSyntax,
|
|
in context: some MacroExpansionContext
|
|
) throws -> [DeclSyntax] {
|
|
guard let identified = declaration.asProtocol(IdentifiedDeclSyntax.self) else {
|
|
return []
|
|
}
|
|
|
|
let parentName = identified.identifier
|
|
|
|
let registrar: DeclSyntax =
|
|
"""
|
|
let _registrar = ObservationRegistrar<\(parentName)>()
|
|
"""
|
|
|
|
let addObserver: DeclSyntax =
|
|
"""
|
|
public nonisolated func addObserver(_ observer: some Observer<\(parentName)>) {
|
|
_registrar.addObserver(observer)
|
|
}
|
|
"""
|
|
|
|
let removeObserver: DeclSyntax =
|
|
"""
|
|
public nonisolated func removeObserver(_ observer: some Observer<\(parentName)>) {
|
|
_registrar.removeObserver(observer)
|
|
}
|
|
"""
|
|
|
|
let withTransaction: DeclSyntax =
|
|
"""
|
|
private func withTransaction<T>(_ apply: () throws -> T) rethrows -> T {
|
|
_registrar.beginAccess()
|
|
defer { _registrar.endAccess() }
|
|
return try apply()
|
|
}
|
|
"""
|
|
|
|
let memberList = MemberDeclListSyntax(
|
|
declaration.members.members.filter {
|
|
$0.decl.isObservableStoredProperty
|
|
}
|
|
)
|
|
|
|
let storageStruct: DeclSyntax =
|
|
"""
|
|
private struct Storage {
|
|
\(memberList)
|
|
}
|
|
"""
|
|
|
|
let storage: DeclSyntax =
|
|
"""
|
|
private var _storage = Storage()
|
|
"""
|
|
|
|
return [
|
|
registrar,
|
|
addObserver,
|
|
removeObserver,
|
|
withTransaction,
|
|
storageStruct,
|
|
storage,
|
|
]
|
|
}
|
|
|
|
// MARK: - MemberAttributeMacro
|
|
|
|
public static func expansion(
|
|
of node: AttributeSyntax,
|
|
attachedTo declaration: some DeclGroupSyntax,
|
|
providingAttributesFor member: some DeclSyntaxProtocol,
|
|
in context: some MacroExpansionContext
|
|
) throws -> [SwiftSyntax.AttributeSyntax] {
|
|
guard member.isObservableStoredProperty else {
|
|
return []
|
|
}
|
|
|
|
return [
|
|
AttributeSyntax(
|
|
attributeName: SimpleTypeIdentifierSyntax(
|
|
name: .identifier("ObservableProperty")
|
|
)
|
|
)
|
|
]
|
|
}
|
|
|
|
}
|
|
|
|
public struct ObservablePropertyMacro: AccessorMacro {
|
|
public static func expansion(
|
|
of node: AttributeSyntax,
|
|
providingAccessorsOf declaration: some DeclSyntaxProtocol,
|
|
in context: some MacroExpansionContext
|
|
) throws -> [AccessorDeclSyntax] {
|
|
guard let property = declaration.as(VariableDeclSyntax.self),
|
|
let binding = property.bindings.first,
|
|
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier,
|
|
binding.accessor == nil
|
|
else {
|
|
return []
|
|
}
|
|
|
|
let getAccessor: AccessorDeclSyntax =
|
|
"""
|
|
get {
|
|
_registrar.beginAccess(\\.\(identifier))
|
|
defer { _registrar.endAccess() }
|
|
return _storage.\(identifier)
|
|
}
|
|
"""
|
|
|
|
let setAccessor: AccessorDeclSyntax =
|
|
"""
|
|
set {
|
|
_registrar.beginAccess(\\.\(identifier))
|
|
_registrar.register(observable: self, willSet: \\.\(identifier), to: newValue)
|
|
defer {
|
|
_registrar.register(observable: self, didSet: \\.\(identifier))
|
|
_registrar.endAccess()
|
|
}
|
|
_storage.\(identifier) = newValue
|
|
}
|
|
"""
|
|
|
|
return [getAccessor, setAccessor]
|
|
}
|
|
}
|
|
|
|
extension DeclModifierSyntax {
|
|
fileprivate var isNeededAccessLevelModifier: Bool {
|
|
switch self.name.tokenKind {
|
|
case .keyword(.public): return true
|
|
default: return false
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SyntaxStringInterpolation {
|
|
fileprivate mutating func appendInterpolation<Node: SyntaxProtocol>(_ node: Node?) {
|
|
if let node {
|
|
appendInterpolation(node)
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct NewTypeMacro: MemberMacro {
|
|
public static func expansion(
|
|
of node: AttributeSyntax,
|
|
providingMembersOf declaration: some DeclGroupSyntax,
|
|
in context: some MacroExpansionContext
|
|
) throws -> [DeclSyntax] {
|
|
guard let type = node.attributeName.as(SimpleTypeIdentifierSyntax.self),
|
|
let genericArguments = type.genericArgumentClause?.arguments,
|
|
genericArguments.count == 1,
|
|
let rawType = genericArguments.first
|
|
else {
|
|
throw CustomError.message(#"@NewType requires the raw type as an argument, in the form "<RawType>"."#)
|
|
}
|
|
|
|
guard let declaration = declaration.as(StructDeclSyntax.self) else {
|
|
throw CustomError.message("@NewType can only be applied to a struct declarations.")
|
|
}
|
|
|
|
let access = declaration.modifiers?.first(where: \.isNeededAccessLevelModifier)
|
|
|
|
return [
|
|
"\(access)typealias RawValue = \(rawType)",
|
|
"\(access)var rawValue: RawValue",
|
|
"\(access)init(_ rawValue: RawValue) { self.rawValue = rawValue }",
|
|
]
|
|
}
|
|
}
|
|
|
|
public struct EmptyMacro: MemberMacro {
|
|
public static func expansion(
|
|
of node: AttributeSyntax,
|
|
providingMembersOf decl: some DeclGroupSyntax,
|
|
in context: some MacroExpansionContext
|
|
) throws -> [DeclSyntax] {
|
|
return []
|
|
}
|
|
}
|
|
|
|
public struct EquatableMacro: ConformanceMacro {
|
|
public static func expansion(
|
|
of node: AttributeSyntax,
|
|
providingConformancesOf decl: some DeclGroupSyntax,
|
|
in context: some MacroExpansionContext
|
|
) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {
|
|
let protocolName: TypeSyntax = "Equatable"
|
|
return [(protocolName, nil)]
|
|
}
|
|
}
|
|
|
|
public struct DelegatedConformanceMacro: ConformanceMacro, MemberMacro {
|
|
public static func expansion(
|
|
of node: AttributeSyntax,
|
|
providingConformancesOf decl: some DeclGroupSyntax,
|
|
in context: some MacroExpansionContext
|
|
) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {
|
|
let protocolName: TypeSyntax = "P"
|
|
let conformance: DeclSyntax =
|
|
"""
|
|
extension Placeholder where Element: P {}
|
|
"""
|
|
|
|
guard let extensionDecl = conformance.as(ExtensionDeclSyntax.self) else {
|
|
return []
|
|
}
|
|
|
|
return [(protocolName, extensionDecl.genericWhereClause)]
|
|
}
|
|
|
|
public static func expansion(
|
|
of node: AttributeSyntax,
|
|
providingMembersOf decl: some DeclGroupSyntax,
|
|
in context: some MacroExpansionContext
|
|
) throws -> [DeclSyntax] {
|
|
let requirement: DeclSyntax =
|
|
"""
|
|
static func requirement() where Element : P {
|
|
Element.requirement()
|
|
}
|
|
"""
|
|
|
|
return [requirement]
|
|
}
|
|
}
|