From 7b3433ffabd2fd6f688660c7e3f4eaf203c6938f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20B=C4=85k?= Date: Sat, 21 Oct 2023 13:55:42 +0200 Subject: [PATCH 1/3] Cherry pick: `Refactor syntax node manipulation to use variable setters` from swift-syntax --- .../MacroExamples/AddAsyncMacro.swift | 97 ++++++++----------- .../MacroExamples/AddBlocker.swift | 16 +-- .../AddCompletionHandlerMacro.swift | 74 +++++++------- .../MacroExamples/CustomCodable.swift | 12 +-- .../DictionaryIndirectionMacro.swift | 7 +- .../MacroExamples/FontLiteralMacro.swift | 8 +- .../MacroExamples/MetaEnumMacro.swift | 7 +- .../MacroExamples/NewTypeMacro.swift | 3 +- .../MacroExamples/OptionSetMacro.swift | 22 ++--- .../WrapStoredPropertiesMacro.swift | 5 +- 10 files changed, 98 insertions(+), 153 deletions(-) diff --git a/Tests/MacroTestingTests/MacroExamples/AddAsyncMacro.swift b/Tests/MacroTestingTests/MacroExamples/AddAsyncMacro.swift index 674e98e..e30eb37 100644 --- a/Tests/MacroTestingTests/MacroExamples/AddAsyncMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/AddAsyncMacro.swift @@ -30,7 +30,7 @@ public struct AddAsyncMacro: PeerMacro { ) throws -> [DeclSyntax] { // Only on functions at the moment. - guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else { + guard var funcDecl = declaration.as(FunctionDeclSyntax.self) else { throw CustomError.message("@addAsync only works on functions") } @@ -42,20 +42,15 @@ public struct AddAsyncMacro: PeerMacro { } // This only makes sense void functions - if funcDecl.signature.returnClause?.type.with(\.leadingTrivia, []).with(\.trailingTrivia, []) - .description != "Void" - { + if funcDecl.signature.returnClause?.type.as(IdentifierTypeSyntax.self)?.name.text != "Void" { throw CustomError.message( "@addAsync requires an function that returns void" ) } // Requires a completion handler block as last parameter - guard - let completionHandlerParameterAttribute = funcDecl.signature.parameterClause.parameters.last? - .type.as(AttributedTypeSyntax.self), - let completionHandlerParameter = completionHandlerParameterAttribute.baseType.as( - FunctionTypeSyntax.self) + guard let completionHandlerParameterAttribute = funcDecl.signature.parameterClause.parameters.last?.type.as(AttributedTypeSyntax.self), + let completionHandlerParameter = completionHandlerParameterAttribute.baseType.as(FunctionTypeSyntax.self) else { throw CustomError.message( "@addAsync requires an function that has a completion handler as last parameter" @@ -63,9 +58,7 @@ public struct AddAsyncMacro: PeerMacro { } // Completion handler needs to return Void - if completionHandlerParameter.returnClause.type.with(\.leadingTrivia, []).with( - \.trailingTrivia, [] - ).description != "Void" { + if completionHandlerParameter.returnClause.type.as(IdentifierTypeSyntax.self)?.name.text != "Void" { throw CustomError.message( "@addAsync requires an function that has a completion handler that returns Void" ) @@ -74,18 +67,16 @@ public struct AddAsyncMacro: PeerMacro { let returnType = completionHandlerParameter.parameters.first?.type let isResultReturn = returnType?.children(viewMode: .all).first?.description == "Result" - let successReturnType = - isResultReturn - ? returnType!.as(IdentifierTypeSyntax.self)!.genericArgumentClause?.arguments.first!.argument - : returnType + let successReturnType = isResultReturn ? returnType!.as(IdentifierTypeSyntax.self)!.genericArgumentClause?.arguments.first!.argument : returnType // Remove completionHandler and comma from the previous parameter var newParameterList = funcDecl.signature.parameterClause.parameters newParameterList.removeLast() - let newParameterListLastParameter = newParameterList.last! + var newParameterListLastParameter = newParameterList.last! newParameterList.removeLast() - newParameterList.append( - newParameterListLastParameter.with(\.trailingTrivia, []).with(\.trailingComma, nil)) + newParameterListLastParameter.trailingTrivia = [] + newParameterListLastParameter.trailingComma = nil + newParameterList.append(newParameterListLastParameter) // Drop the @addAsync attribute from the new declaration. let newAttributeList = funcDecl.attributes.filter { @@ -132,44 +123,36 @@ public struct AddAsyncMacro: PeerMacro { """ - let newFunc = - funcDecl - .with( - \.signature, - funcDecl.signature - .with( - \.effectSpecifiers, - FunctionEffectSpecifiersSyntax( - leadingTrivia: .space, - asyncSpecifier: .keyword(.async), - throwsSpecifier: isResultReturn ? .keyword(.throws) : nil - ) // add async - ) - .with( - \.returnClause, - successReturnType != nil - ? ReturnClauseSyntax( - leadingTrivia: .space, type: successReturnType!.with(\.leadingTrivia, .space)) : nil - ) // add result type - .with( - \.parameterClause, - funcDecl.signature.parameterClause.with(\.parameters, newParameterList) // drop completion handler - .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)) + // add async + funcDecl.signature.effectSpecifiers = FunctionEffectSpecifiersSyntax( + leadingTrivia: .space, + asyncSpecifier: .keyword(.async), + throwsSpecifier: isResultReturn ? .keyword(.throws) : nil + ) + + // add result type + if let successReturnType { + funcDecl.signature.returnClause = ReturnClauseSyntax(leadingTrivia: .space, type: successReturnType.with(\.leadingTrivia, .space)) + } else { + funcDecl.signature.returnClause = nil + } + + // drop completion handler + funcDecl.signature.parameterClause.parameters = newParameterList + funcDecl.signature.parameterClause.trailingTrivia = [] + + funcDecl.body = CodeBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space), + statements: CodeBlockItemListSyntax( + [CodeBlockItemSyntax(item: .expr(newBody))] + ), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + + funcDecl.attributes = newAttributeList + + funcDecl.leadingTrivia = .newlines(2) - return [DeclSyntax(newFunc)] + return [DeclSyntax(funcDecl)] } } diff --git a/Tests/MacroTestingTests/MacroExamples/AddBlocker.swift b/Tests/MacroTestingTests/MacroExamples/AddBlocker.swift index b0db043..0bffb04 100644 --- a/Tests/MacroTestingTests/MacroExamples/AddBlocker.swift +++ b/Tests/MacroTestingTests/MacroExamples/AddBlocker.swift @@ -27,7 +27,7 @@ public struct AddBlocker: ExpressionMacro { _ node: InfixOperatorExprSyntax ) -> ExprSyntax { // Identify any infix operator + in the tree. - if let binOp = node.operator.as(BinaryOperatorExprSyntax.self) { + if var binOp = node.operator.as(BinaryOperatorExprSyntax.self) { if binOp.operator.text == "+" { // Form the warning let messageID = MessageID(domain: "silly", id: "addblock") @@ -72,17 +72,9 @@ public struct AddBlocker: ExpressionMacro { ) ) - return ExprSyntax( - node.with( - \.operator, - ExprSyntax( - binOp.with( - \.operator, - binOp.operator.with(\.tokenKind, .binaryOperator("-")) - ) - ) - ) - ) + binOp.operator.tokenKind = .binaryOperator("-") + + return ExprSyntax(node.with(\.operator, ExprSyntax(binOp))) } } diff --git a/Tests/MacroTestingTests/MacroExamples/AddCompletionHandlerMacro.swift b/Tests/MacroTestingTests/MacroExamples/AddCompletionHandlerMacro.swift index a70aa08..0691f23 100644 --- a/Tests/MacroTestingTests/MacroExamples/AddCompletionHandlerMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/AddCompletionHandlerMacro.swift @@ -25,20 +25,22 @@ public struct AddCompletionHandlerMacro: PeerMacro { ) 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 { + guard var 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 { - let newEffects: FunctionEffectSpecifiersSyntax + var newEffects: FunctionEffectSpecifiersSyntax if let existingEffects = funcDecl.signature.effectSpecifiers { - newEffects = existingEffects.with(\.asyncSpecifier, .keyword(.async)) + newEffects = existingEffects + newEffects.asyncSpecifier = .keyword(.async) } else { newEffects = FunctionEffectSpecifiersSyntax(asyncSpecifier: .keyword(.async)) } - let newSignature = funcDecl.signature.with(\.effectSpecifiers, newEffects) + var newSignature = funcDecl.signature + newSignature.effectSpecifiers = newEffects let messageID = MessageID(domain: "MacroExamples", id: "MissingAsync") let diag = Diagnostic( @@ -73,8 +75,9 @@ public struct AddCompletionHandlerMacro: PeerMacro { } // Form the completion handler parameter. - let resultType: TypeSyntax? = funcDecl.signature.returnClause?.type.with(\.leadingTrivia, []) - .with(\.trailingTrivia, []) + var resultType = funcDecl.signature.returnClause?.type + resultType?.leadingTrivia = [] + resultType?.trailingTrivia = [] let completionHandlerParam = FunctionParameterSyntax( @@ -86,14 +89,12 @@ public struct AddCompletionHandlerMacro: PeerMacro { // Add the completion handler parameter to the parameter list. let parameterList = funcDecl.signature.parameterClause.parameters var newParameterList = parameterList - if let lastParam = parameterList.last { + if var lastParam = parameterList.last { // We need to add a trailing comma to the preceding list. newParameterList.removeLast() + lastParam.trailingComma = .commaToken(trailingTrivia: .space) newParameterList += [ - lastParam.with( - \.trailingComma, - .commaToken(trailingTrivia: .space) - ), + lastParam, completionHandlerParam, ] } else { @@ -137,35 +138,28 @@ public struct AddCompletionHandlerMacro: PeerMacro { 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(\.returnClause, nil) // drop result type - .with( - \.parameterClause, // add completion handler parameter - funcDecl.signature.parameterClause.with(\.parameters, 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)) + // drop async + funcDecl.signature.effectSpecifiers?.asyncSpecifier = nil + + // drop result type + funcDecl.signature.returnClause = nil + + // add completion handler parameter + funcDecl.signature.parameterClause.parameters = newParameterList + funcDecl.signature.parameterClause.trailingTrivia = [] + + funcDecl.body = CodeBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space), + statements: CodeBlockItemListSyntax( + [CodeBlockItemSyntax(item: .expr(newBody))] + ), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + + funcDecl.attributes = newAttributeList + + funcDecl.leadingTrivia = .newlines(2) - return [DeclSyntax(newFunc)] + return [DeclSyntax(funcDecl)] } } diff --git a/Tests/MacroTestingTests/MacroExamples/CustomCodable.swift b/Tests/MacroTestingTests/MacroExamples/CustomCodable.swift index a3b1d6a..1b4fffe 100644 --- a/Tests/MacroTestingTests/MacroExamples/CustomCodable.swift +++ b/Tests/MacroTestingTests/MacroExamples/CustomCodable.swift @@ -24,22 +24,18 @@ public enum CustomCodable: MemberMacro { let cases = memberList.compactMap({ member -> String? in // is a property guard - let propertyName = member.decl.as(VariableDeclSyntax.self)?.bindings.first?.pattern.as( - IdentifierPatternSyntax.self)?.identifier.text + let propertyName = member.decl.as(VariableDeclSyntax.self)?.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier.text else { return nil } // if it has a CodableKey macro on it - if let customKeyMacro = member.decl.as(VariableDeclSyntax.self)?.attributes.first(where: { - element in - element.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.description - == "CodableKey" + if let customKeyMacro = member.decl.as(VariableDeclSyntax.self)?.attributes.first(where: { element in + element.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.description == "CodableKey" }) { // Uses the value in the Macro - let customKeyValue = customKeyMacro.as(AttributeSyntax.self)!.arguments!.as( - LabeledExprListSyntax.self)!.first!.expression + let customKeyValue = customKeyMacro.as(AttributeSyntax.self)!.arguments!.as(LabeledExprListSyntax.self)!.first!.expression return "case \(propertyName) = \(customKeyValue)" } else { diff --git a/Tests/MacroTestingTests/MacroExamples/DictionaryIndirectionMacro.swift b/Tests/MacroTestingTests/MacroExamples/DictionaryIndirectionMacro.swift index 23d7ce4..ccd3651 100644 --- a/Tests/MacroTestingTests/MacroExamples/DictionaryIndirectionMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/DictionaryIndirectionMacro.swift @@ -21,10 +21,7 @@ extension DictionaryStorageMacro: MemberMacro { providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { - let storage: DeclSyntax = "var _storage: [String: Any] = [:]" - return [ - storage.with(\.leadingTrivia, [.newlines(1), .spaces(2)]) - ] + return ["\n var _storage: [String: Any] = [:]"] } } @@ -43,11 +40,11 @@ extension DictionaryStorageMacro: MemberAttributeMacro { return [ AttributeSyntax( + leadingTrivia: [.newlines(1), .spaces(2)], attributeName: IdentifierTypeSyntax( name: .identifier("DictionaryStorageProperty") ) ) - .with(\.leadingTrivia, [.newlines(1), .spaces(2)]) ] } } diff --git a/Tests/MacroTestingTests/MacroExamples/FontLiteralMacro.swift b/Tests/MacroTestingTests/MacroExamples/FontLiteralMacro.swift index 97aedda..52fcd75 100644 --- a/Tests/MacroTestingTests/MacroExamples/FontLiteralMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/FontLiteralMacro.swift @@ -35,14 +35,12 @@ private func replaceFirstLabel( of tuple: LabeledExprListSyntax, with newLabel: String ) -> LabeledExprListSyntax { - guard let firstElement = tuple.first else { + if tuple.isEmpty { return tuple } var tuple = tuple - tuple[tuple.startIndex] = - firstElement - .with(\.label, .identifier(newLabel)) - .with(\.colon, .colonToken()) + tuple[tuple.startIndex].label = .identifier(newLabel) + tuple[tuple.startIndex].colon = .colonToken() return tuple } diff --git a/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift b/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift index 22ac266..0352e9d 100644 --- a/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift @@ -21,9 +21,7 @@ public struct MetaEnumMacro { let access: DeclModifierListSyntax.Element? let parentParamName: TokenSyntax - init( - node: AttributeSyntax, declaration: some DeclGroupSyntax, context: some MacroExpansionContext - ) throws { + init(node: AttributeSyntax, declaration: some DeclGroupSyntax, context: some MacroExpansionContext) throws { guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { throw DiagnosticsError(diagnostics: [ CaseMacroDiagnostic.notAnEnum(declaration).diagnose(at: Syntax(node)) @@ -112,8 +110,7 @@ extension CaseMacroDiagnostic: DiagnosticMessage { var message: String { switch self { case .notAnEnum(let decl): - return - "'@MetaEnum' can only be attached to an enum, not \(decl.descriptiveDeclKind(withArticle: true))" + return "'@MetaEnum' can only be attached to an enum, not \(decl.descriptiveDeclKind(withArticle: true))" } } diff --git a/Tests/MacroTestingTests/MacroExamples/NewTypeMacro.swift b/Tests/MacroTestingTests/MacroExamples/NewTypeMacro.swift index 41e8cdd..1740aaf 100644 --- a/Tests/MacroTestingTests/MacroExamples/NewTypeMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/NewTypeMacro.swift @@ -30,8 +30,7 @@ extension NewTypeMacro: MemberMacro { .expression.as(MemberAccessExprSyntax.self), let rawType = memberAccessExn.base?.as(DeclReferenceExprSyntax.self) else { - throw CustomError.message( - #"@NewType requires the raw type as an argument, in the form "RawType.self"."#) + throw CustomError.message(#"@NewType requires the raw type as an argument, in the form "RawType.self"."#) } guard let declaration = declaration.as(StructDeclSyntax.self) else { diff --git a/Tests/MacroTestingTests/MacroExamples/OptionSetMacro.swift b/Tests/MacroTestingTests/MacroExamples/OptionSetMacro.swift index f34c9e8..b868721 100644 --- a/Tests/MacroTestingTests/MacroExamples/OptionSetMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/OptionSetMacro.swift @@ -91,9 +91,7 @@ public struct OptionSetMacro { stringLiteral.segments.count == 1, case let .stringSegment(optionsEnumNameString)? = stringLiteral.segments.first else { - context.diagnose( - OptionSetMacroDiagnostic.requiresStringLiteral(optionsEnumNameArgumentLabel).diagnose( - at: optionEnumNameArg.expression)) + context.diagnose(OptionSetMacroDiagnostic.requiresStringLiteral(optionsEnumNameArgumentLabel).diagnose(at: optionEnumNameArg.expression)) return nil } @@ -120,15 +118,12 @@ public struct OptionSetMacro { return nil }).first else { - context.diagnose( - OptionSetMacroDiagnostic.requiresOptionsEnum(optionsEnumName).diagnose(at: decl)) + context.diagnose(OptionSetMacroDiagnostic.requiresOptionsEnum(optionsEnumName).diagnose(at: decl)) return nil } // Retrieve the raw type from the attribute. - guard - let genericArgs = attribute.attributeName.as(IdentifierTypeSyntax.self)? - .genericArgumentClause, + guard let genericArgs = attribute.attributeName.as(IdentifierTypeSyntax.self)?.genericArgumentClause, let rawType = genericArgs.arguments.first?.argument else { context.diagnose(OptionSetMacroDiagnostic.requiresOptionsEnumRawType.diagnose(at: attribute)) @@ -148,16 +143,13 @@ extension OptionSetMacro: ExtensionMacro { in context: some MacroExpansionContext ) throws -> [ExtensionDeclSyntax] { // Decode the expansion arguments. - guard let (structDecl, _, _) = decodeExpansion(of: node, attachedTo: declaration, in: context) - else { + guard let (structDecl, _, _) = decodeExpansion(of: node, attachedTo: declaration, in: context) else { return [] } // If there is an explicit conformance to OptionSet already, don't add one. if let inheritedTypes = structDecl.inheritanceClause?.inheritedTypes, - inheritedTypes.contains( - where: { inherited in inherited.type.trimmedDescription == "OptionSet" } - ) + inheritedTypes.contains(where: { inherited in inherited.type.trimmedDescription == "OptionSet" }) { return [] } @@ -173,9 +165,7 @@ extension OptionSetMacro: MemberMacro { in context: some MacroExpansionContext ) throws -> [DeclSyntax] { // Decode the expansion arguments. - guard - let (_, optionsEnum, rawType) = decodeExpansion(of: attribute, attachedTo: decl, in: context) - else { + guard let (_, optionsEnum, rawType) = decodeExpansion(of: attribute, attachedTo: decl, in: context) else { return [] } diff --git a/Tests/MacroTestingTests/MacroExamples/WrapStoredPropertiesMacro.swift b/Tests/MacroTestingTests/MacroExamples/WrapStoredPropertiesMacro.swift index 5f34c39..edb4890 100644 --- a/Tests/MacroTestingTests/MacroExamples/WrapStoredPropertiesMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/WrapStoredPropertiesMacro.swift @@ -43,17 +43,16 @@ public struct WrapStoredPropertiesMacro: MemberAttributeMacro { 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") + throw CustomError.message("macro requires a string literal containing the name of an attribute") } return [ AttributeSyntax( + leadingTrivia: [.newlines(1), .spaces(2)], attributeName: IdentifierTypeSyntax( name: .identifier(wrapperName.content.text) ) ) - .with(\.leadingTrivia, [.newlines(1), .spaces(2)]) ] } } From 42d70aa7021c3ca90820d85d5e6b7aa6b3105a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20B=C4=85k?= Date: Sat, 21 Oct 2023 13:56:12 +0200 Subject: [PATCH 2/3] Cherry pick: `Add extension macro example ` from swift-syntax --- ...ltFatalErrorImplementationMacroTests.swift | 69 ++++++++++++++++++ ...DefaultFatalErrorImplementationMacro.swift | 72 +++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 Tests/MacroTestingTests/DefaultFatalErrorImplementationMacroTests.swift create mode 100644 Tests/MacroTestingTests/MacroExamples/DefaultFatalErrorImplementationMacro.swift diff --git a/Tests/MacroTestingTests/DefaultFatalErrorImplementationMacroTests.swift b/Tests/MacroTestingTests/DefaultFatalErrorImplementationMacroTests.swift new file mode 100644 index 0000000..5e5b47b --- /dev/null +++ b/Tests/MacroTestingTests/DefaultFatalErrorImplementationMacroTests.swift @@ -0,0 +1,69 @@ +import MacroTesting +import XCTest + +final class DefaultFatalErrorImplementationMacroTests: BaseTestCase { + override func invokeTest() { + withMacroTesting( + macros: ["defaultFatalErrorImplementation": DefaultFatalErrorImplementationMacro.self] + ) { + super.invokeTest() + } + } + + func testExpansionWhenAttachedToProtocolExpandsCorrectly() { + assertMacro { + """ + @defaultFatalErrorImplementation + protocol MyProtocol { + func foo() + func bar() -> Int + } + """ + } expansion: { + """ + protocol MyProtocol { + func foo() + func bar() -> Int + } + + extension MyProtocol { + func foo() { + fatalError("whoops 😅") + } + func bar() -> Int { + fatalError("whoops 😅") + } + } + """ + } + } + + func testExpansionWhenNotAttachedToProtocolProducesDiagnostic() { + assertMacro { + """ + @defaultFatalErrorImplementation + class MyClass {} + """ + } diagnostics: { + """ + @defaultFatalErrorImplementation + ┬─────────────────────────────── + ╰─ 🛑 Macro `defaultFatalErrorImplementation` can only be applied to a protocol + class MyClass {} + """ + } + } + + func testExpansionWhenAttachedToEmptyProtocolDoesNotAddExtension() { + assertMacro { + """ + @defaultFatalErrorImplementation + protocol EmptyProtocol {} + """ + } expansion: { + """ + protocol EmptyProtocol {} + """ + } + } +} diff --git a/Tests/MacroTestingTests/MacroExamples/DefaultFatalErrorImplementationMacro.swift b/Tests/MacroTestingTests/MacroExamples/DefaultFatalErrorImplementationMacro.swift new file mode 100644 index 0000000..c1e9fb1 --- /dev/null +++ b/Tests/MacroTestingTests/MacroExamples/DefaultFatalErrorImplementationMacro.swift @@ -0,0 +1,72 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +/// Provides default `fatalError` implementations for protocol methods. +/// +/// This macro generates extensions that add default `fatalError` implementations +/// for each method in the protocol it is attached to. +public enum DefaultFatalErrorImplementationMacro: ExtensionMacro { + + /// Unique identifier for messages related to this macro. + private static let messageID = MessageID(domain: "MacroExamples", id: "ProtocolDefaultImplementation") + + /// Generates extension for the protocol to which this macro is attached. + public static func expansion( + of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [ExtensionDeclSyntax] { + + // Validate that the macro is being applied to a protocol declaration + guard let protocolDecl = declaration.as(ProtocolDeclSyntax.self) else { + throw SimpleDiagnosticMessage( + message: "Macro `defaultFatalErrorImplementation` can only be applied to a protocol", + diagnosticID: messageID, + severity: .error + ) + } + + // Extract all the methods from the protocol and assign default implementations + let methods = protocolDecl.memberBlock.members + .map(\.decl) + .compactMap { declaration -> FunctionDeclSyntax? in + guard var function = declaration.as(FunctionDeclSyntax.self) else { + return nil + } + function.body = CodeBlockSyntax { + ExprSyntax(#"fatalError("whoops 😅")"#) + } + return function + } + + // Don't generate an extension if there are no methods + if methods.isEmpty { + return [] + } + + // Generate the extension containing the default implementations + let extensionDecl = ExtensionDeclSyntax(extendedType: type) { + for method in methods { + MemberBlockItemSyntax(decl: method) + } + } + + return [extensionDecl] + } +} From 15cf156cd5c25310ea39f45bfdeffea53b6f4a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20B=C4=85k?= Date: Sun, 22 Oct 2023 08:57:16 +0200 Subject: [PATCH 3/3] Incorporate ObservableMacro --- .../MacroExamples/ObservableMacro.swift | 173 ++++++++++++++++++ .../ObservableMacroTests.swift | 132 +++++++++++++ 2 files changed, 305 insertions(+) create mode 100644 Tests/MacroTestingTests/MacroExamples/ObservableMacro.swift create mode 100644 Tests/MacroTestingTests/ObservableMacroTests.swift diff --git a/Tests/MacroTestingTests/MacroExamples/ObservableMacro.swift b/Tests/MacroTestingTests/MacroExamples/ObservableMacro.swift new file mode 100644 index 0000000..ec4669f --- /dev/null +++ b/Tests/MacroTestingTests/MacroExamples/ObservableMacro.swift @@ -0,0 +1,173 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax +import SwiftSyntaxMacros + +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.accessorBlock == 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(NamedDeclSyntax.self) else { + return [] + } + + let parentName = identified.name + + 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(_ apply: () throws -> T) rethrows -> T { + _registrar.beginAccess() + defer { _registrar.endAccess() } + return try apply() + } + """ + + let memberList = declaration.memberBlock.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: IdentifierTypeSyntax( + name: .identifier("ObservableProperty") + ) + ) + ] + } + +} + +extension ObservableMacro: ExtensionMacro { + public static func expansion( + of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [ExtensionDeclSyntax] { + [try ExtensionDeclSyntax("extension \(type): Observable {}")] + } +} + +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.accessorBlock == 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] + } +} diff --git a/Tests/MacroTestingTests/ObservableMacroTests.swift b/Tests/MacroTestingTests/ObservableMacroTests.swift new file mode 100644 index 0000000..f05027f --- /dev/null +++ b/Tests/MacroTestingTests/ObservableMacroTests.swift @@ -0,0 +1,132 @@ +import MacroTesting +import XCTest + +final class ObservableMacroTests: XCTestCase { + override func invokeTest() { + withMacroTesting( + macros: [ + "Observable": ObservableMacro.self, + "ObservableProperty": ObservablePropertyMacro.self + ] + ) { + super.invokeTest() + } + } + + func testExpansion() { + assertMacro { + """ + @Observable + final class Dog { + var name: String? + var treat: Treat? + + var isHappy: Bool = true + + init() {} + + func bark() { + print("bork bork") + } + } + """ + } expansion: { + #""" + final class Dog { + var name: String? { + get { + _registrar.beginAccess(\.name) + defer { + _registrar.endAccess() + } + return _storage.name + } + set { + _registrar.beginAccess(\.name) + _registrar.register(observable: self, willSet: \.name, to: newValue) + defer { + _registrar.register(observable: self, didSet: \.name) + _registrar.endAccess() + } + _storage.name = newValue + } + } + var treat: Treat? { + get { + _registrar.beginAccess(\.treat) + defer { + _registrar.endAccess() + } + return _storage.treat + } + set { + _registrar.beginAccess(\.treat) + _registrar.register(observable: self, willSet: \.treat, to: newValue) + defer { + _registrar.register(observable: self, didSet: \.treat) + _registrar.endAccess() + } + _storage.treat = newValue + } + } + + var isHappy: Bool = true { + get { + _registrar.beginAccess(\.isHappy) + defer { + _registrar.endAccess() + } + return _storage.isHappy + } + set { + _registrar.beginAccess(\.isHappy) + _registrar.register(observable: self, willSet: \.isHappy, to: newValue) + defer { + _registrar.register(observable: self, didSet: \.isHappy) + _registrar.endAccess() + } + _storage.isHappy = newValue + } + } + + init() {} + + func bark() { + print("bork bork") + } + + let _registrar = ObservationRegistrar() + + public nonisolated func addObserver(_ observer: some Observer) { + _registrar.addObserver(observer) + } + + public nonisolated func removeObserver(_ observer: some Observer) { + _registrar.removeObserver(observer) + } + + private func withTransaction(_ apply: () throws -> T) rethrows -> T { + _registrar.beginAccess() + defer { + _registrar.endAccess() + } + return try apply() + } + + private struct Storage { + + var name: String? + var treat: Treat? + + var isHappy: Bool = true + } + + private var _storage = Storage() + } + + extension Dog: Observable { + } + """# + } + } +}