Skip to content

Commit 29c0ca5

Browse files
authored
[Macros] Introduce conformsTo in assertMacroExpansion
Allows testing extension macros completely
1 parent 0d3f3e3 commit 29c0ca5

File tree

4 files changed

+199
-19
lines changed

4 files changed

+199
-19
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import SwiftSyntax
2+
import SwiftSyntaxMacros
3+
4+
/// The specification for a macro.
5+
public struct MacroSpec {
6+
/// The type of macro.
7+
let type: Macro.Type
8+
/// The list of types macro needs to add conformances.
9+
let conformances: [TypeSyntax]
10+
11+
/// Creates a new specification from provided macro type
12+
/// and optional list of generated conformances.
13+
///
14+
/// - Parameters:
15+
/// - type: The type of macro.
16+
/// - conformances: The list of types macro needs to add conformances.
17+
public init(type: Macro.Type, conformances: [TypeSyntax] = []) {
18+
self.type = type
19+
self.conformances = conformances
20+
}
21+
}

Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,23 @@ extension SyntaxProtocol {
2525
macros: [String: Macro.Type],
2626
in context: some MacroExpansionContext,
2727
indentationWidth: Trivia? = nil
28+
) -> Syntax {
29+
let specs = Dictionary(uniqueKeysWithValues: macros.map { ($0.key, MacroSpec(type: $0.value)) })
30+
return self.expand(macroSpecs: specs, in: context, indentationWidth: indentationWidth)
31+
}
32+
33+
/// Expand all uses of the given set of macros with specifications within this syntax node.
34+
public func expand(
35+
macroSpecs: [String: MacroSpec],
36+
in context: some MacroExpansionContext,
37+
indentationWidth: Trivia? = nil
2838
) -> Syntax {
2939
// Build the macro system.
3040
var system = MacroSystem()
31-
for (macroName, macroType) in macros {
32-
try! system.add(macroType, name: macroName)
41+
for (macroName, macroSpec) in macroSpecs {
42+
try! system.add(macroSpec.type, name: macroName)
43+
let conformedTypes = InheritedTypeListSyntax(macroSpec.conformances.map { InheritedTypeSyntax(type: $0) })
44+
try! system.add(conformedTypes, name: macroName)
3345
}
3446

3547
let applier = MacroApplication(
@@ -316,6 +328,7 @@ private func expandExtensionMacro(
316328
definition: ExtensionMacro.Type,
317329
attributeNode: AttributeSyntax,
318330
attachedTo: DeclSyntax,
331+
conformanceList: InheritedTypeListSyntax?,
319332
in context: some MacroExpansionContext,
320333
indentationWidth: Trivia
321334
) throws -> CodeBlockItemListSyntax? {
@@ -336,7 +349,7 @@ private func expandExtensionMacro(
336349
declarationNode: attachedTo.detach(in: context),
337350
parentDeclNode: nil,
338351
extendedType: extendedType.detach(in: context),
339-
conformanceList: [],
352+
conformanceList: conformanceList ?? [],
340353
in: context,
341354
indentationWidth: indentationWidth
342355
)
@@ -355,11 +368,14 @@ private func expandExtensionMacro(
355368
enum MacroSystemError: Error {
356369
/// Indicates that a macro with the given name has already been defined.
357370
case alreadyDefined(new: Macro.Type, existing: Macro.Type)
371+
/// Indicates that protocol conformances for a macro with the given name has already been defined.
372+
case alreadyConforming(new: InheritedTypeListSyntax, existing: InheritedTypeListSyntax)
358373
}
359374

360375
/// A system of known macros that can be expanded syntactically
361376
struct MacroSystem {
362377
var macros: [String: Macro.Type] = [:]
378+
var conformanceMap: [String: InheritedTypeListSyntax] = [:]
363379

364380
/// Create an empty macro system.
365381
init() {}
@@ -375,10 +391,26 @@ struct MacroSystem {
375391
macros[name] = macro
376392
}
377393

394+
/// Add protocol conformances for a macro to the system.
395+
///
396+
/// Throws an error if there is already conformances for a macro with this name.
397+
mutating func add(_ conformanceList: InheritedTypeListSyntax, name: String) throws {
398+
if let knownConformanceList = conformanceMap[name] {
399+
throw MacroSystemError.alreadyConforming(new: conformanceList, existing: knownConformanceList)
400+
}
401+
402+
conformanceMap[name] = conformanceList
403+
}
404+
378405
/// Look for a macro with the given name.
379406
func lookup(_ macroName: String) -> Macro.Type? {
380407
return macros[macroName]
381408
}
409+
410+
/// Look for protocol conformances of a macro with the given name.
411+
func conformaces(forMacro macroName: String) -> InheritedTypeListSyntax? {
412+
return conformanceMap[macroName]
413+
}
382414
}
383415

384416
// MARK: - MacroApplication
@@ -729,12 +761,13 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
729761
// MARK: Attached macro expansions.
730762

731763
extension MacroApplication {
732-
/// Get pairs of a macro attribute and the macro definition attached to `decl`.
764+
/// Get macro attribute, the macro definition and optional
765+
/// conformance protocols list attached to `decl`.
733766
///
734767
/// The macros must be registered in `macroSystem`.
735768
private func macroAttributes(
736769
attachedTo decl: DeclSyntax
737-
) -> [(attributeNode: AttributeSyntax, definition: Macro.Type)] {
770+
) -> [(attributeNode: AttributeSyntax, definition: Macro.Type, conformanceList: InheritedTypeListSyntax?)] {
738771
guard let attributedNode = decl.asProtocol(WithAttributesSyntax.self) else {
739772
return []
740773
}
@@ -747,22 +780,22 @@ extension MacroApplication {
747780
return nil
748781
}
749782

750-
return (attribute, macro)
783+
return (attribute, macro, macroSystem.conformaces(forMacro: attributeName))
751784
}
752785
}
753786

754-
/// Get pairs of a macro attribute and the macro definition attached to `decl`
755-
/// matching `ofType` macro type.
787+
/// Get macro attribute, the macro definition and optional conformance
788+
/// protocols list attached to `decl` matching `ofType` macro type.
756789
///
757790
/// The macros must be registered in `macroSystem`.
758791
private func macroAttributes<MacroType>(
759792
attachedTo decl: DeclSyntax,
760793
ofType: MacroType.Type
761-
) -> [(attributeNode: AttributeSyntax, definition: MacroType)] {
794+
) -> [(attributeNode: AttributeSyntax, definition: MacroType, conformanceList: InheritedTypeListSyntax?)] {
762795
return macroAttributes(attachedTo: decl)
763-
.compactMap { (attributeNode: AttributeSyntax, definition: Macro.Type) in
796+
.compactMap { (attributeNode: AttributeSyntax, definition: Macro.Type, conformanceList: InheritedTypeListSyntax?) in
764797
if let macroType = definition as? MacroType {
765-
return (attributeNode, macroType)
798+
return (attributeNode, macroType, conformanceList)
766799
} else {
767800
return nil
768801
}
@@ -778,13 +811,13 @@ extension MacroApplication {
778811
>(
779812
attachedTo decl: DeclSyntax,
780813
ofType: MacroType.Type,
781-
expandMacro: (_ attributeNode: AttributeSyntax, _ definition: MacroType) throws -> ExpanedNodeCollection?
814+
expandMacro: (_ attributeNode: AttributeSyntax, _ definition: MacroType, _ conformanceList: InheritedTypeListSyntax?) throws -> ExpanedNodeCollection?
782815
) -> [ExpandedNode] {
783816
var result: [ExpandedNode] = []
784817

785818
for macroAttribute in macroAttributes(attachedTo: decl, ofType: ofType) {
786819
do {
787-
if let expanded = try expandMacro(macroAttribute.attributeNode, macroAttribute.definition) {
820+
if let expanded = try expandMacro(macroAttribute.attributeNode, macroAttribute.definition, macroAttribute.conformanceList) {
788821
result += expanded
789822
}
790823
} catch {
@@ -802,7 +835,7 @@ extension MacroApplication {
802835
///
803836
/// - Returns: The macro-synthesized peers
804837
private func expandMemberDeclPeers(of decl: DeclSyntax) -> [MemberBlockItemSyntax] {
805-
return expandMacros(attachedTo: decl, ofType: PeerMacro.Type.self) { attributeNode, definition in
838+
return expandMacros(attachedTo: decl, ofType: PeerMacro.Type.self) { attributeNode, definition, conformanceList in
806839
return try expandPeerMacroMember(
807840
definition: definition,
808841
attributeNode: attributeNode,
@@ -822,7 +855,7 @@ extension MacroApplication {
822855
///
823856
/// - Returns: The macro-synthesized peers
824857
private func expandCodeBlockPeers(of decl: DeclSyntax) -> [CodeBlockItemSyntax] {
825-
return expandMacros(attachedTo: decl, ofType: PeerMacro.Type.self) { attributeNode, definition in
858+
return expandMacros(attachedTo: decl, ofType: PeerMacro.Type.self) { attributeNode, definition, conformanceList in
826859
return try expandPeerMacroCodeItem(
827860
definition: definition,
828861
attributeNode: attributeNode,
@@ -837,11 +870,12 @@ extension MacroApplication {
837870
///
838871
/// - Returns: The macro-synthesized extensions
839872
private func expandExtensions(of decl: DeclSyntax) -> [CodeBlockItemSyntax] {
840-
return expandMacros(attachedTo: decl, ofType: ExtensionMacro.Type.self) { attributeNode, definition in
873+
return expandMacros(attachedTo: decl, ofType: ExtensionMacro.Type.self) { attributeNode, definition, conformanceList in
841874
return try expandExtensionMacro(
842875
definition: definition,
843876
attributeNode: attributeNode,
844877
attachedTo: decl,
878+
conformanceList: conformanceList,
845879
in: context,
846880
indentationWidth: indentationWidth
847881
)
@@ -850,7 +884,7 @@ extension MacroApplication {
850884

851885
/// Expand all 'member' macros attached to `decl`.
852886
private func expandMembers(of decl: DeclSyntax) -> [MemberBlockItemSyntax] {
853-
return expandMacros(attachedTo: decl, ofType: MemberMacro.Type.self) { attributeNode, definition in
887+
return expandMacros(attachedTo: decl, ofType: MemberMacro.Type.self) { attributeNode, definition, conformanceList in
854888
return try expandMemberMacro(
855889
definition: definition,
856890
attributeNode: attributeNode,
@@ -870,7 +904,7 @@ extension MacroApplication {
870904
of decl: DeclSyntax,
871905
parentDecl: DeclSyntax
872906
) -> [AttributeListSyntax.Element] {
873-
return expandMacros(attachedTo: parentDecl, ofType: MemberAttributeMacro.Type.self) { attributeNode, definition in
907+
return expandMacros(attachedTo: parentDecl, ofType: MemberAttributeMacro.Type.self) { attributeNode, definition, conformanceList in
874908
return try expandMemberAttributeMacro(
875909
definition: definition,
876910
attributeNode: attributeNode,

Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,50 @@ public func assertMacroExpansion(
271271
indentationWidth: Trivia = .spaces(4),
272272
file: StaticString = #file,
273273
line: UInt = #line
274+
) {
275+
let specs = Dictionary(uniqueKeysWithValues: macros.map { ($0.key, MacroSpec(type: $0.value)) })
276+
assertMacroExpansion(
277+
originalSource,
278+
expandedSource: expectedExpandedSource,
279+
diagnostics: diagnostics,
280+
macroSpecs: specs,
281+
applyFixIts: applyFixIts,
282+
fixedSource: expectedFixedSource,
283+
testModuleName: testModuleName,
284+
testFileName: testFileName,
285+
indentationWidth: indentationWidth
286+
)
287+
}
288+
289+
/// Assert that expanding the given macros in the original source produces
290+
/// the given expanded source code.
291+
///
292+
/// - Parameters:
293+
/// - originalSource: The original source code, which is expected to contain
294+
/// macros in various places (e.g., `#stringify(x + y)`).
295+
/// - expectedExpandedSource: The source code that we expect to see after
296+
/// performing macro expansion on the original source.
297+
/// - diagnostics: The diagnostics when expanding any macro
298+
/// - macroSpecs: The macros that should be expanded, provided as a dictionary
299+
/// mapping macro names (e.g., `"stringify"`) to specification
300+
/// (e.g., `StringifyMacro.self`).
301+
/// - applyFixIts: If specified, filters the Fix-Its that are applied to generate `fixedSource` to only those whose message occurs in this array. If `nil`, all Fix-Its from the diagnostics are applied.
302+
/// - fixedSource: If specified, asserts that the source code after applying Fix-Its matches this string.
303+
/// - testModuleName: The name of the test module to use.
304+
/// - testFileName: The name of the test file name to use.
305+
/// - indentationWidth: The indentation width used in the expansion.
306+
public func assertMacroExpansion(
307+
_ originalSource: String,
308+
expandedSource expectedExpandedSource: String,
309+
diagnostics: [DiagnosticSpec] = [],
310+
macroSpecs: [String: MacroSpec],
311+
applyFixIts: [String]? = nil,
312+
fixedSource expectedFixedSource: String? = nil,
313+
testModuleName: String = "TestModule",
314+
testFileName: String = "test.swift",
315+
indentationWidth: Trivia = .spaces(4),
316+
file: StaticString = #file,
317+
line: UInt = #line
274318
) {
275319
// Parse the original source file.
276320
let origSourceFile = Parser.parse(source: originalSource)
@@ -280,7 +324,7 @@ public func assertMacroExpansion(
280324
sourceFiles: [origSourceFile: .init(moduleName: testModuleName, fullFilePath: testFileName)]
281325
)
282326

283-
let expandedSourceFile = origSourceFile.expand(macros: macros, in: context, indentationWidth: indentationWidth)
327+
let expandedSourceFile = origSourceFile.expand(macroSpecs: macroSpecs, in: context, indentationWidth: indentationWidth)
284328
let diags = ParseDiagnosticsGenerator.diagnostics(for: expandedSourceFile)
285329
if !diags.isEmpty {
286330
XCTFail(

Tests/SwiftSyntaxMacroExpansionTest/ExtensionMacroTests.swift

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,85 @@ 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+
macroSpecs: ["AddCodableExtensions": MacroSpec(type: CodableExtensionMacro.self, conformances: ["Decodable", "Encodable"])],
170+
indentationWidth: indentationWidth
171+
)
172+
173+
assertMacroExpansion(
174+
"""
175+
struct Wrapper {
176+
@AddCodableExtensions
177+
struct MyType {
178+
}
179+
}
180+
""",
181+
expandedSource: """
182+
struct Wrapper {
183+
struct MyType {
184+
}
185+
}
186+
187+
extension MyType: Encodable {
188+
}
189+
""",
190+
macroSpecs: ["AddCodableExtensions": MacroSpec(type: CodableExtensionMacro.self, conformances: ["Encodable"])],
191+
indentationWidth: indentationWidth
192+
)
193+
194+
assertMacroExpansion(
195+
"""
196+
struct Wrapper {
197+
@AddCodableExtensions
198+
struct MyType {
199+
}
200+
}
201+
""",
202+
expandedSource: """
203+
struct Wrapper {
204+
struct MyType {
205+
}
206+
}
207+
""",
208+
macros: ["AddCodableExtensions": CodableExtensionMacro.self],
209+
indentationWidth: indentationWidth
210+
)
211+
}
131212
}

0 commit comments

Comments
 (0)