diff --git a/Examples/Sources/MacroExamples/Implementation/Extension/DefaultFatalErrorImplementationMacro.swift b/Examples/Sources/MacroExamples/Implementation/Extension/DefaultFatalErrorImplementationMacro.swift new file mode 100644 index 00000000000..c1e9fb13199 --- /dev/null +++ b/Examples/Sources/MacroExamples/Implementation/Extension/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] + } +} diff --git a/Examples/Sources/MacroExamples/Implementation/Plugin.swift b/Examples/Sources/MacroExamples/Implementation/Plugin.swift index a123cc05b6c..e5960e80712 100644 --- a/Examples/Sources/MacroExamples/Implementation/Plugin.swift +++ b/Examples/Sources/MacroExamples/Implementation/Plugin.swift @@ -23,6 +23,7 @@ struct MyPlugin: CompilerPlugin { CaseDetectionMacro.self, CodableKey.self, CustomCodable.self, + DefaultFatalErrorImplementationMacro.self, DictionaryStorageMacro.self, DictionaryStoragePropertyMacro.self, EquatableExtensionMacro.self, diff --git a/Examples/Sources/MacroExamples/Interface/ExtensionMacros.swift b/Examples/Sources/MacroExamples/Interface/ExtensionMacros.swift index 73f5c8e05a5..3b8bde56e2c 100644 --- a/Examples/Sources/MacroExamples/Interface/ExtensionMacros.swift +++ b/Examples/Sources/MacroExamples/Interface/ExtensionMacros.swift @@ -14,3 +14,43 @@ @attached(extension, conformances: Equatable) public macro equatable() = #externalMacro(module: "MacroExamplesImplementation", type: "EquatableExtensionMacro") + +// MARK: - Default Fatal Error Implementation + +/// A macro that provides default `fatalError` implementations for protocol methods. +/// +/// This macro generates Swift extensions for the attached protocol, +/// adding a default `fatalError` implementation for each method defined within it. +/// +/// ## Example usage: +/// ```swift +/// @defaultFatalErrorImplementation +/// protocol MyProtocol { +/// func someMethod() +/// } +/// ``` +/// +/// The generated code would look like: +/// ```swift +/// protocol MyProtocol { +/// func someMethod() +/// } +/// +/// extension MyProtocol { +/// func someMethod() { +/// fatalError("whoops 😅") +/// } +/// } +/// ``` +/// +/// ## Edge Cases +/// - **No Methods in Protocol**: If the protocol does not contain any methods, +/// the macro will not generate an empty extension. +/// - **Incorrect Attachment**: If the macro is attached to a non-protocol declaration, +/// it will produce an error diagnostic stating that it can only be applied to a protocol. +@attached(extension, names: arbitrary) +public macro defaultFatalErrorImplementation() = + #externalMacro( + module: "MacroExamplesImplementation", + type: "DefaultFatalErrorImplementationMacro" + ) diff --git a/Examples/Sources/MacroExamples/Playground/ExtensionMacrosPlayground.swift b/Examples/Sources/MacroExamples/Playground/ExtensionMacrosPlayground.swift index 272ff6be723..2d0003ab435 100644 --- a/Examples/Sources/MacroExamples/Playground/ExtensionMacrosPlayground.swift +++ b/Examples/Sources/MacroExamples/Playground/ExtensionMacrosPlayground.swift @@ -12,6 +12,22 @@ import MacroExamplesInterface +// MARK: - Default Fatal Error Implementation + +@defaultFatalErrorImplementation +protocol API { + func getItems() -> [String] + func removeItem(id: String) +} + +struct MyAPI: API {} + +func runDefaultFatalErrorImplementationMacroPlayground() { + let myAPI = MyAPI() + + print("Implementation of `API` protocol with default implementation: \(myAPI)") +} + // MARK: - Equatable Extension @equatable diff --git a/Examples/Sources/MacroExamples/Playground/main.swift b/Examples/Sources/MacroExamples/Playground/main.swift index 410907a4ebd..d9af26d3e0e 100644 --- a/Examples/Sources/MacroExamples/Playground/main.swift +++ b/Examples/Sources/MacroExamples/Playground/main.swift @@ -32,6 +32,8 @@ runExpressionMacrosPlayground() // MARK: - Extension Macros +runDefaultFatalErrorImplementationMacroPlayground() + runEquatableExtensionMacroPlayground() // MARK: - Member Attribute Macros diff --git a/Examples/Tests/MacroExamples/Implementation/Extension/DefaultFatalErrorImplementationMacroTests.swift b/Examples/Tests/MacroExamples/Implementation/Extension/DefaultFatalErrorImplementationMacroTests.swift new file mode 100644 index 00000000000..fcd5cfa29e1 --- /dev/null +++ b/Examples/Tests/MacroExamples/Implementation/Extension/DefaultFatalErrorImplementationMacroTests.swift @@ -0,0 +1,84 @@ +//===----------------------------------------------------------------------===// +// +// 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 MacroExamplesImplementation +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +final class DefaultFatalErrorImplementationMacroTests: XCTestCase { + private let macros = ["defaultFatalErrorImplementation": DefaultFatalErrorImplementationMacro.self] + + func testExpansionWhenAttachedToProtocolExpandsCorrectly() { + assertMacroExpansion( + """ + @defaultFatalErrorImplementation + protocol MyProtocol { + func foo() + func bar() -> Int + } + """, + expandedSource: """ + protocol MyProtocol { + func foo() + func bar() -> Int + } + + extension MyProtocol { + func foo() { + fatalError("whoops 😅") + } + func bar() -> Int { + fatalError("whoops 😅") + } + } + """, + macros: macros, + indentationWidth: .spaces(2) + ) + } + + func testExpansionWhenNotAttachedToProtocolProducesDiagnostic() { + assertMacroExpansion( + """ + @defaultFatalErrorImplementation + class MyClass {} + """, + expandedSource: """ + class MyClass {} + """, + diagnostics: [ + DiagnosticSpec( + message: "Macro `defaultFatalErrorImplementation` can only be applied to a protocol", + line: 1, + column: 1 + ) + ], + macros: macros, + indentationWidth: .spaces(2) + ) + } + + func testExpansionWhenAttachedToEmptyProtocolDoesNotAddExtension() { + assertMacroExpansion( + """ + @defaultFatalErrorImplementation + protocol EmptyProtocol {} + """, + expandedSource: """ + protocol EmptyProtocol {} + """, + macros: macros, + indentationWidth: .spaces(2) + ) + } +}