Skip to content

Commit 63a8530

Browse files
committed
Add extension macro example
This commit introduces a new macro, `ProtocolDefaultImplMacro`, 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.
1 parent 1fa18e5 commit 63a8530

File tree

6 files changed

+214
-0
lines changed

6 files changed

+214
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
import SwiftSyntaxMacros
15+
import SwiftSyntaxBuilder
16+
import SwiftDiagnostics
17+
18+
/// Provides default `fatalError` implementations for protocol methods.
19+
///
20+
/// This macro generates extensions that add default `fatalError` implementations
21+
/// for each method in the protocol it is attached to.
22+
public enum DefaultFatalErrorImplementationMacro: ExtensionMacro {
23+
24+
/// Unique identifier for messages related to this macro.
25+
private static let messageID = MessageID(domain: "MacroExamples", id: "ProtocolDefaultImplementation")
26+
27+
/// Generates extension for the protocol to which this macro is attached.
28+
public static func expansion(
29+
of node: AttributeSyntax,
30+
attachedTo declaration: some DeclGroupSyntax,
31+
providingExtensionsOf type: some TypeSyntaxProtocol,
32+
conformingTo protocols: [TypeSyntax],
33+
in context: some MacroExpansionContext
34+
) throws -> [ExtensionDeclSyntax] {
35+
36+
// Validate that the macro is being applied to a protocol declaration
37+
guard let protocolDecl = declaration.as(ProtocolDeclSyntax.self) else {
38+
throw SimpleDiagnosticMessage(
39+
message: "Macro `defaultFatalErrorImplementation` can only be applied to a protocol",
40+
diagnosticID: messageID,
41+
severity: .error
42+
)
43+
}
44+
45+
// Extract all the methods from the protocol and assign default implementations
46+
let methods = protocolDecl.memberBlock.members
47+
.map(\.decl)
48+
.compactMap { declaration -> FunctionDeclSyntax? in
49+
guard var function = declaration.as(FunctionDeclSyntax.self) else {
50+
return nil
51+
}
52+
function.body = CodeBlockSyntax {
53+
ExprSyntax(#"fatalError("whoops 😅")"#)
54+
}
55+
return function
56+
}
57+
58+
// Don't generate an extension if there are no methods
59+
if methods.isEmpty {
60+
return []
61+
}
62+
63+
// Generate the extension containing the default implementations
64+
let extensionDecl = ExtensionDeclSyntax(extendedType: type) {
65+
for method in methods {
66+
MemberBlockItemSyntax(decl: method)
67+
}
68+
}
69+
70+
return [extensionDecl]
71+
}
72+
}

Examples/Sources/MacroExamples/Implementation/Plugin.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ struct MyPlugin: CompilerPlugin {
2323
CaseDetectionMacro.self,
2424
CodableKey.self,
2525
CustomCodable.self,
26+
DefaultFatalErrorImplementationMacro.self,
2627
DictionaryStorageMacro.self,
2728
DictionaryStoragePropertyMacro.self,
2829
EquatableExtensionMacro.self,

Examples/Sources/MacroExamples/Interface/ExtensionMacros.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,42 @@
1414

1515
@attached(extension, conformances: Equatable)
1616
public macro equatable() = #externalMacro(module: "MacroExamplesImplementation", type: "EquatableExtensionMacro")
17+
18+
// MARK: - Default Fatal Error Implementation
19+
20+
/// A macro that provides default `fatalError` implementations for protocol methods.
21+
///
22+
/// This macro generates Swift extensions for the attached protocol,
23+
/// adding a default `fatalError` implementation for each method defined within it.
24+
///
25+
/// ## Example usage:
26+
/// ```swift
27+
/// @defaultFatalErrorImplementation
28+
/// protocol MyProtocol {
29+
/// func someMethod()
30+
/// }
31+
/// ```
32+
///
33+
/// The generated code would look like:
34+
/// ```swift
35+
/// protocol MyProtocol {
36+
/// func someMethod()
37+
/// }
38+
///
39+
/// extension MyProtocol {
40+
/// func someMethod() {
41+
/// fatalError("whoops 😅")
42+
/// }
43+
/// }
44+
/// ```
45+
///
46+
/// ## Edge Cases
47+
/// - **No Methods in Protocol**: If the protocol does not contain any methods,
48+
/// the macro will not generate an empty extension.
49+
/// - **Incorrect Attachment**: If the macro is attached to a non-protocol declaration,
50+
/// it will produce an error diagnostic stating that it can only be applied to a protocol.
51+
@attached(extension, names: arbitrary)
52+
public macro defaultFatalErrorImplementation() = #externalMacro(
53+
module: "MacroExamplesImplementation",
54+
type: "DefaultFatalErrorImplementationMacro"
55+
)

Examples/Sources/MacroExamples/Playground/ExtensionMacrosPlayground.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,22 @@
1212

1313
import MacroExamplesInterface
1414

15+
// MARK: - Default Fatal Error Implementation
16+
17+
@defaultFatalErrorImplementation
18+
protocol API {
19+
func getItems() -> [String]
20+
func removeItem(id: String)
21+
}
22+
23+
struct MyAPI: API {}
24+
25+
func runDefaultFatalErrorImplementationMacroPlayground() {
26+
let myAPI = MyAPI()
27+
28+
print("Implementation of `API` protocol with default implementation: \(myAPI)")
29+
}
30+
1531
// MARK: - Equatable Extension
1632

1733
@equatable

Examples/Sources/MacroExamples/Playground/main.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ runExpressionMacrosPlayground()
3232

3333
// MARK: - Extension Macros
3434

35+
runDefaultFatalErrorImplementationMacroPlayground()
36+
3537
runEquatableExtensionMacroPlayground()
3638

3739
// MARK: - Member Attribute Macros
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import MacroExamplesImplementation
14+
import SwiftSyntaxMacros
15+
import SwiftSyntaxMacrosTestSupport
16+
import XCTest
17+
18+
final class DefaultFatalErrorImplementationMacroTests: XCTestCase {
19+
private let macros = ["defaultFatalErrorImplementation": DefaultFatalErrorImplementationMacro.self]
20+
21+
func testExpansionWhenAttachedToProtocolExpandsCorrectly() {
22+
assertMacroExpansion(
23+
"""
24+
@defaultFatalErrorImplementation
25+
protocol MyProtocol {
26+
func foo()
27+
func bar() -> Int
28+
}
29+
""",
30+
expandedSource: """
31+
protocol MyProtocol {
32+
func foo()
33+
func bar() -> Int
34+
}
35+
36+
extension MyProtocol {
37+
func foo() {
38+
fatalError("whoops 😅")
39+
}
40+
func bar() -> Int {
41+
fatalError("whoops 😅")
42+
}
43+
}
44+
""",
45+
macros: macros,
46+
indentationWidth: .spaces(2)
47+
)
48+
}
49+
50+
func testExpansionWhenNotAttachedToProtocolProducesDiagnostic() {
51+
assertMacroExpansion(
52+
"""
53+
@defaultFatalErrorImplementation
54+
class MyClass {}
55+
""",
56+
expandedSource: """
57+
class MyClass {}
58+
""",
59+
diagnostics: [
60+
DiagnosticSpec(
61+
message: "Macro `defaultFatalErrorImplementation` can only be applied to a protocol",
62+
line: 1,
63+
column: 1
64+
)
65+
],
66+
macros: macros,
67+
indentationWidth: .spaces(2)
68+
)
69+
}
70+
71+
func testExpansionWhenAttachedToEmptyProtocolDoesNotAddExtension() {
72+
assertMacroExpansion(
73+
"""
74+
@defaultFatalErrorImplementation
75+
protocol EmptyProtocol {}
76+
""",
77+
expandedSource: """
78+
protocol EmptyProtocol {}
79+
""",
80+
macros: macros,
81+
indentationWidth: .spaces(2)
82+
)
83+
}
84+
}

0 commit comments

Comments
 (0)