Skip to content

Commit 10fc603

Browse files
authored
[Macros] Introduce conformsTo in assertMacroExpansion
Allows testing extension macros completely
1 parent 7cff260 commit 10fc603

File tree

3 files changed

+133
-17
lines changed

3 files changed

+133
-17
lines changed

Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ extension SyntaxProtocol {
2323
/// Expand all uses of the given set of macros within this syntax node.
2424
public func expand(
2525
macros: [String: Macro.Type],
26+
conformsTo conformanceMap: [String: InheritedTypeListSyntax],
2627
in context: some MacroExpansionContext,
2728
indentationWidth: Trivia? = nil
2829
) -> Syntax {
@@ -31,6 +32,9 @@ extension SyntaxProtocol {
3132
for (macroName, macroType) in macros {
3233
try! system.add(macroType, name: macroName)
3334
}
35+
for (macroName, conformanceList) in conformanceMap {
36+
try! system.add(conformanceList, name: macroName)
37+
}
3438

3539
let applier = MacroApplication(
3640
macroSystem: system,
@@ -316,6 +320,7 @@ private func expandExtensionMacro(
316320
definition: ExtensionMacro.Type,
317321
attributeNode: AttributeSyntax,
318322
attachedTo: DeclSyntax,
323+
conformanceList: InheritedTypeListSyntax?,
319324
in context: some MacroExpansionContext,
320325
indentationWidth: Trivia
321326
) throws -> CodeBlockItemListSyntax? {
@@ -336,7 +341,7 @@ private func expandExtensionMacro(
336341
declarationNode: attachedTo.detach(in: context),
337342
parentDeclNode: nil,
338343
extendedType: extendedType.detach(in: context),
339-
conformanceList: [],
344+
conformanceList: conformanceList ?? [],
340345
in: context,
341346
indentationWidth: indentationWidth
342347
)
@@ -355,11 +360,14 @@ private func expandExtensionMacro(
355360
enum MacroSystemError: Error {
356361
/// Indicates that a macro with the given name has already been defined.
357362
case alreadyDefined(new: Macro.Type, existing: Macro.Type)
363+
/// Indicates that protocol conformances for a macro with the given name has already been defined.
364+
case alreadyConforming(new: InheritedTypeListSyntax, existing: InheritedTypeListSyntax)
358365
}
359366

360367
/// A system of known macros that can be expanded syntactically
361368
struct MacroSystem {
362369
var macros: [String: Macro.Type] = [:]
370+
var conformanceMap: [String: InheritedTypeListSyntax] = [:]
363371

364372
/// Create an empty macro system.
365373
init() {}
@@ -375,10 +383,26 @@ struct MacroSystem {
375383
macros[name] = macro
376384
}
377385

386+
/// Add protocol conformances for a macro to the system.
387+
///
388+
/// Throws an error if there is already conformances for a macro with this name.
389+
mutating func add(_ conformanceList: InheritedTypeListSyntax, name: String) throws {
390+
if let knownConformanceList = conformanceMap[name] {
391+
throw MacroSystemError.alreadyConforming(new: conformanceList, existing: knownConformanceList)
392+
}
393+
394+
conformanceMap[name] = conformanceList
395+
}
396+
378397
/// Look for a macro with the given name.
379398
func lookup(_ macroName: String) -> Macro.Type? {
380399
return macros[macroName]
381400
}
401+
402+
/// Look for protocol conformances of a macro with the given name.
403+
func conformaces(forMacro macroName: String) -> InheritedTypeListSyntax? {
404+
return conformanceMap[macroName]
405+
}
382406
}
383407

384408
// MARK: - MacroApplication
@@ -716,12 +740,13 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
716740
// MARK: Attached macro expansions.
717741

718742
extension MacroApplication {
719-
/// Get pairs of a macro attribute and the macro definition attached to `decl`.
743+
/// Get macro attribute, the macro definition and optional
744+
/// conformance protocols list attached to `decl`.
720745
///
721746
/// The macros must be registered in `macroSystem`.
722747
private func macroAttributes(
723748
attachedTo decl: DeclSyntax
724-
) -> [(attributeNode: AttributeSyntax, definition: Macro.Type)] {
749+
) -> [(attributeNode: AttributeSyntax, definition: Macro.Type, conformanceList: InheritedTypeListSyntax?)] {
725750
guard let attributedNode = decl.asProtocol(WithAttributesSyntax.self) else {
726751
return []
727752
}
@@ -734,22 +759,22 @@ extension MacroApplication {
734759
return nil
735760
}
736761

737-
return (attribute, macro)
762+
return (attribute, macro, macroSystem.conformaces(forMacro: attributeName))
738763
}
739764
}
740765

741-
/// Get pairs of a macro attribute and the macro definition attached to `decl`
742-
/// matching `ofType` macro type.
766+
/// Get macro attribute, the macro definition and optional conformance
767+
/// protocols list attached to `decl` matching `ofType` macro type.
743768
///
744769
/// The macros must be registered in `macroSystem`.
745770
private func macroAttributes<MacroType>(
746771
attachedTo decl: DeclSyntax,
747772
ofType: MacroType.Type
748-
) -> [(attributeNode: AttributeSyntax, definition: MacroType)] {
773+
) -> [(attributeNode: AttributeSyntax, definition: MacroType, conformanceList: InheritedTypeListSyntax?)] {
749774
return macroAttributes(attachedTo: decl)
750-
.compactMap { (attributeNode: AttributeSyntax, definition: Macro.Type) in
775+
.compactMap { (attributeNode: AttributeSyntax, definition: Macro.Type, conformanceList: InheritedTypeListSyntax?) in
751776
if let macroType = definition as? MacroType {
752-
return (attributeNode, macroType)
777+
return (attributeNode, macroType, conformanceList)
753778
} else {
754779
return nil
755780
}
@@ -765,13 +790,13 @@ extension MacroApplication {
765790
>(
766791
attachedTo decl: DeclSyntax,
767792
ofType: MacroType.Type,
768-
expandMacro: (_ attributeNode: AttributeSyntax, _ definition: MacroType) throws -> ExpanedNodeCollection?
793+
expandMacro: (_ attributeNode: AttributeSyntax, _ definition: MacroType, _ conformanceList: InheritedTypeListSyntax?) throws -> ExpanedNodeCollection?
769794
) -> [ExpandedNode] {
770795
var result: [ExpandedNode] = []
771796

772797
for macroAttribute in macroAttributes(attachedTo: decl, ofType: ofType) {
773798
do {
774-
if let expanded = try expandMacro(macroAttribute.attributeNode, macroAttribute.definition) {
799+
if let expanded = try expandMacro(macroAttribute.attributeNode, macroAttribute.definition, macroAttribute.conformanceList) {
775800
result += expanded
776801
}
777802
} catch {
@@ -789,7 +814,7 @@ extension MacroApplication {
789814
///
790815
/// - Returns: The macro-synthesized peers
791816
private func expandMemberDeclPeers(of decl: DeclSyntax) -> [MemberBlockItemSyntax] {
792-
return expandMacros(attachedTo: decl, ofType: PeerMacro.Type.self) { attributeNode, definition in
817+
return expandMacros(attachedTo: decl, ofType: PeerMacro.Type.self) { attributeNode, definition, conformanceList in
793818
return try expandPeerMacroMember(
794819
definition: definition,
795820
attributeNode: attributeNode,
@@ -809,7 +834,7 @@ extension MacroApplication {
809834
///
810835
/// - Returns: The macro-synthesized peers
811836
private func expandCodeBlockPeers(of decl: DeclSyntax) -> [CodeBlockItemSyntax] {
812-
return expandMacros(attachedTo: decl, ofType: PeerMacro.Type.self) { attributeNode, definition in
837+
return expandMacros(attachedTo: decl, ofType: PeerMacro.Type.self) { attributeNode, definition, conformanceList in
813838
return try expandPeerMacroCodeItem(
814839
definition: definition,
815840
attributeNode: attributeNode,
@@ -824,11 +849,12 @@ extension MacroApplication {
824849
///
825850
/// - Returns: The macro-synthesized extensions
826851
private func expandExtensions(of decl: DeclSyntax) -> [CodeBlockItemSyntax] {
827-
return expandMacros(attachedTo: decl, ofType: ExtensionMacro.Type.self) { attributeNode, definition in
852+
return expandMacros(attachedTo: decl, ofType: ExtensionMacro.Type.self) { attributeNode, definition, conformanceList in
828853
return try expandExtensionMacro(
829854
definition: definition,
830855
attributeNode: attributeNode,
831856
attachedTo: decl,
857+
conformanceList: conformanceList,
832858
in: context,
833859
indentationWidth: indentationWidth
834860
)
@@ -837,7 +863,7 @@ extension MacroApplication {
837863

838864
/// Expand all 'member' macros attached to `decl`.
839865
private func expandMembers(of decl: DeclSyntax) -> [MemberBlockItemSyntax] {
840-
return expandMacros(attachedTo: decl, ofType: MemberMacro.Type.self) { attributeNode, definition in
866+
return expandMacros(attachedTo: decl, ofType: MemberMacro.Type.self) { attributeNode, definition, conformanceList in
841867
return try expandMemberMacro(
842868
definition: definition,
843869
attributeNode: attributeNode,
@@ -857,7 +883,7 @@ extension MacroApplication {
857883
of decl: DeclSyntax,
858884
parentDecl: DeclSyntax
859885
) -> [AttributeListSyntax.Element] {
860-
return expandMacros(attachedTo: parentDecl, ofType: MemberAttributeMacro.Type.self) { attributeNode, definition in
886+
return expandMacros(attachedTo: parentDecl, ofType: MemberAttributeMacro.Type.self) { attributeNode, definition, conformanceList in
861887
return try expandMemberAttributeMacro(
862888
definition: definition,
863889
attributeNode: attributeNode,

Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,13 +254,17 @@ func assertDiagnostic(
254254
/// - macros: The macros that should be expanded, provided as a dictionary
255255
/// mapping macro names (e.g., `"stringify"`) to implementation types
256256
/// (e.g., `StringifyMacro.self`).
257+
/// - conformanceMap: The protocols that maco should conform to, provided as
258+
/// a dictionary mapping macro names (e.g., `"AddCodable"`) to protocols names
259+
/// (e.g., `"Decodable"`, `"Encodable"` etc.).
257260
/// - testModuleName: The name of the test module to use.
258261
/// - testFileName: The name of the test file name to use.
259262
public func assertMacroExpansion(
260263
_ originalSource: String,
261264
expandedSource expectedExpandedSource: String,
262265
diagnostics: [DiagnosticSpec] = [],
263266
macros: [String: Macro.Type],
267+
conformsTo conformanceMap: [String: [TypeSyntax]] = [:],
264268
testModuleName: String = "TestModule",
265269
testFileName: String = "test.swift",
266270
indentationWidth: Trivia = .spaces(4),
@@ -275,7 +279,10 @@ public func assertMacroExpansion(
275279
sourceFiles: [origSourceFile: .init(moduleName: testModuleName, fullFilePath: testFileName)]
276280
)
277281

278-
let expandedSourceFile = origSourceFile.expand(macros: macros, in: context, indentationWidth: indentationWidth)
282+
let conformanceMap: [String: InheritedTypeListSyntax] = Dictionary(
283+
uniqueKeysWithValues: conformanceMap.map { ($0.key, InheritedTypeListSyntax($0.value.map { InheritedTypeSyntax(type: $0) })) }
284+
)
285+
let expandedSourceFile = origSourceFile.expand(macros: macros, conformsTo: conformanceMap, in: context, indentationWidth: indentationWidth)
279286
let diags = ParseDiagnosticsGenerator.diagnostics(for: expandedSourceFile)
280287
if !diags.isEmpty {
281288
XCTFail(

Tests/SwiftSyntaxMacroExpansionTest/ExtensionMacroTests.swift

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,87 @@ final class ExtensionMacroTests: XCTestCase {
128128
]
129129
)
130130
}
131+
132+
func testConditionalExtensionExpansion() {
133+
struct CodableExtensionMacro: ExtensionMacro {
134+
static func expansion(
135+
of node: AttributeSyntax,
136+
attachedTo: some DeclGroupSyntax,
137+
providingExtensionsOf type: some TypeSyntaxProtocol,
138+
conformingTo protocols: [TypeSyntax],
139+
in context: some MacroExpansionContext
140+
) throws -> [ExtensionDeclSyntax] {
141+
let extensions: [ExtensionDeclSyntax] = protocols.compactMap { `protocol` in
142+
let decl = """
143+
extension \(type.trimmed): \(`protocol`) {}
144+
""" as DeclSyntax
145+
return decl.as(ExtensionDeclSyntax.self)
146+
}
147+
148+
return extensions
149+
}
150+
}
151+
152+
assertMacroExpansion(
153+
"""
154+
@AddCodableExtensions
155+
struct MyType {
156+
}
157+
""",
158+
expandedSource: """
159+
160+
struct MyType {
161+
}
162+
163+
extension MyType: Decodable {
164+
}
165+
166+
extension MyType: Encodable {
167+
}
168+
""",
169+
macros: ["AddCodableExtensions": CodableExtensionMacro.self],
170+
conformsTo: ["AddCodableExtensions": ["Decodable", "Encodable"]],
171+
indentationWidth: indentationWidth
172+
)
173+
174+
assertMacroExpansion(
175+
"""
176+
struct Wrapper {
177+
@AddCodableExtensions
178+
struct MyType {
179+
}
180+
}
181+
""",
182+
expandedSource: """
183+
struct Wrapper {
184+
struct MyType {
185+
}
186+
}
187+
188+
extension MyType: Encodable {
189+
}
190+
""",
191+
macros: ["AddCodableExtensions": CodableExtensionMacro.self],
192+
conformsTo: ["AddCodableExtensions": ["Encodable"]],
193+
indentationWidth: indentationWidth
194+
)
195+
196+
assertMacroExpansion(
197+
"""
198+
struct Wrapper {
199+
@AddCodableExtensions
200+
struct MyType {
201+
}
202+
}
203+
""",
204+
expandedSource: """
205+
struct Wrapper {
206+
struct MyType {
207+
}
208+
}
209+
""",
210+
macros: ["AddCodableExtensions": CodableExtensionMacro.self],
211+
indentationWidth: indentationWidth
212+
)
213+
}
131214
}

0 commit comments

Comments
 (0)