Skip to content

Commit

Permalink
Add extension macro example
Browse files Browse the repository at this point in the history
This commit introduces a new macro, `DefaultFatalErrorImplementationMacro`, to provide default implementations for protocol methods. The macro automatically generates extension code blocks with the default implementations for any protocol it's attached to.
  • Loading branch information
Matejkob committed Oct 19, 2023
1 parent 1fa18e5 commit c39dcf7
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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 SwiftSyntax
import SwiftSyntaxMacros
import SwiftSyntaxBuilder
import SwiftDiagnostics

/// 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]
}
}
1 change: 1 addition & 0 deletions Examples/Sources/MacroExamples/Implementation/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ struct MyPlugin: CompilerPlugin {
CaseDetectionMacro.self,
CodableKey.self,
CustomCodable.self,
DefaultFatalErrorImplementationMacro.self,
DictionaryStorageMacro.self,
DictionaryStoragePropertyMacro.self,
EquatableExtensionMacro.self,
Expand Down
39 changes: 39 additions & 0 deletions Examples/Sources/MacroExamples/Interface/ExtensionMacros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,42 @@

@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"
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions Examples/Sources/MacroExamples/Playground/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ runExpressionMacrosPlayground()

// MARK: - Extension Macros

runDefaultFatalErrorImplementationMacroPlayground()

runEquatableExtensionMacroPlayground()

// MARK: - Member Attribute Macros
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
)
}
}

0 comments on commit c39dcf7

Please sign in to comment.