Skip to content

Add extension macro example #2305

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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]
}
}
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
40 changes: 40 additions & 0 deletions Examples/Sources/MacroExamples/Interface/ExtensionMacros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
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)
)
}
}