Skip to content

[5.9][Macros] Pass a list of protocols to conform to when expanding an extension macro. #1873

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 7 commits into from
Jul 3, 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
5 changes: 5 additions & 0 deletions Examples/Sources/ExamplePlugin/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,13 @@ public struct SendableExtensionMacro: ExtensionMacro {
of node: AttributeSyntax,
attachedTo: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
if protocols.isEmpty {
return []
}

let sendableExtension: DeclSyntax =
"""
extension \(type.trimmed): Sendable {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,25 @@ extension CompilerPluginMessageHandler {
expandingSyntax: expandingSyntax
)

case .expandAttachedMacro(let macro, let macroRole, let discriminator, let attributeSyntax, let declSyntax, let parentDeclSyntax, let extendedTypeSyntax):
case .expandAttachedMacro(
let macro,
let macroRole,
let discriminator,
let attributeSyntax,
let declSyntax,
let parentDeclSyntax,
let extendedTypeSyntax,
let conformanceListSyntax
):
try expandAttachedMacro(
macro: macro,
macroRole: macroRole,
discriminator: discriminator,
attributeSyntax: attributeSyntax,
declSyntax: declSyntax,
parentDeclSyntax: parentDeclSyntax,
extendedTypeSyntax: extendedTypeSyntax
extendedTypeSyntax: extendedTypeSyntax,
conformanceListSyntax: conformanceListSyntax
)

case .loadPluginLibrary(let libraryPath, let moduleName):
Expand Down
8 changes: 7 additions & 1 deletion Sources/SwiftCompilerPluginMessageHandling/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ extension CompilerPluginMessageHandler {
attributeSyntax: PluginMessage.Syntax,
declSyntax: PluginMessage.Syntax,
parentDeclSyntax: PluginMessage.Syntax?,
extendedTypeSyntax: PluginMessage.Syntax?
extendedTypeSyntax: PluginMessage.Syntax?,
conformanceListSyntax: PluginMessage.Syntax?
) throws {
let sourceManager = SourceManager()
let context = PluginMacroExpansionContext(
Expand All @@ -104,6 +105,10 @@ extension CompilerPluginMessageHandler {
let extendedType = extendedTypeSyntax.map {
sourceManager.add($0).cast(TypeSyntax.self)
}
let conformanceList = conformanceListSyntax.map {
let placeholderStruct = sourceManager.add($0).cast(StructDeclSyntax.self)
return placeholderStruct.inheritanceClause!.inheritedTypeCollection
}

// TODO: Make this a 'String?' and remove non-'hasExpandMacroResult' branches.
let expandedSources: [String]?
Expand All @@ -120,6 +125,7 @@ extension CompilerPluginMessageHandler {
declarationNode: declarationNode,
parentDeclNode: parentDeclNode,
extendedType: extendedType,
conformanceList: conformanceList,
in: context
)
if let expansions, hostCapability.hasExpandMacroResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
attributeSyntax: PluginMessage.Syntax,
declSyntax: PluginMessage.Syntax,
parentDeclSyntax: PluginMessage.Syntax?,
extendedTypeSyntax: PluginMessage.Syntax?
extendedTypeSyntax: PluginMessage.Syntax?,
conformanceListSyntax: PluginMessage.Syntax?
)

/// Optionally implemented message to load a dynamic link library.
Expand Down Expand Up @@ -76,7 +77,7 @@
}

@_spi(PluginMessage) public enum PluginMessage {
public static var PROTOCOL_VERSION_NUMBER: Int { 6 } // Added 'expandMacroResult'.
public static var PROTOCOL_VERSION_NUMBER: Int { 7 } // Pass extension protocol list

public struct HostCapability: Codable {
var protocolVersion: Int
Expand Down
42 changes: 6 additions & 36 deletions Sources/SwiftSyntaxMacroExpansion/MacroExpansion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ public func expandAttachedMacroWithoutCollapsing<Context: MacroExpansionContext>
declarationNode: DeclSyntax,
parentDeclNode: DeclSyntax?,
extendedType: TypeSyntax?,
conformanceList: InheritedTypeListSyntax?,
in context: Context
) -> [String]? {
do {
Expand Down Expand Up @@ -266,42 +267,6 @@ public func expandAttachedMacroWithoutCollapsing<Context: MacroExpansionContext>
$0.formattedExpansion(definition.formatMode)
}

case (let attachedMacro as ConformanceMacro.Type, .conformance):
guard let declGroup = declarationNode.asProtocol(DeclGroupSyntax.self) else {
// Compiler error: type mismatch.
throw MacroExpansionError.declarationNotDeclGroup
}
guard let identified = declarationNode.asProtocol(IdentifiedDeclSyntax.self)
else {
// Compiler error: type mismatch.
throw MacroExpansionError.declarationNotIdentified
}

// Local function to expand a conformance macro once we've opened up
// the existential.
func expandConformanceMacro(
_ node: some DeclGroupSyntax
) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {
return try attachedMacro.expansion(
of: attributeNode,
providingConformancesOf: node,
in: context
)
}

let conformances = try _openExistential(
declGroup,
do: expandConformanceMacro
)

// Form a buffer of extension declarations to return to the caller.
return conformances.map { typeSyntax, whereClause in
let typeName = identified.identifier.trimmedDescription
let protocolName = typeSyntax.trimmedDescription
let whereClause = whereClause?.trimmedDescription ?? ""
return "extension \(typeName) : \(protocolName) \(whereClause) {}"
}

case (let attachedMacro as ExtensionMacro.Type, .extension):
guard let declGroup = declarationNode.asProtocol(DeclGroupSyntax.self) else {
// Compiler error: type mismatch.
Expand All @@ -312,6 +277,8 @@ public func expandAttachedMacroWithoutCollapsing<Context: MacroExpansionContext>
throw MacroExpansionError.noExtendedTypeSyntax
}

let protocols = conformanceList?.map(\.typeName) ?? []

// Local function to expand an extension macro once we've opened up
// the existential.
func expandExtensionMacro(
Expand All @@ -321,6 +288,7 @@ public func expandAttachedMacroWithoutCollapsing<Context: MacroExpansionContext>
of: attributeNode,
attachedTo: node,
providingExtensionsOf: extendedType,
conformingTo: protocols,
in: context
)
}
Expand Down Expand Up @@ -364,6 +332,7 @@ public func expandAttachedMacro<Context: MacroExpansionContext>(
declarationNode: DeclSyntax,
parentDeclNode: DeclSyntax?,
extendedType: TypeSyntax?,
conformanceList: InheritedTypeListSyntax?,
in context: Context
) -> String? {
let expandedSources = expandAttachedMacroWithoutCollapsing(
Expand All @@ -373,6 +342,7 @@ public func expandAttachedMacro<Context: MacroExpansionContext>(
declarationNode: declarationNode,
parentDeclNode: parentDeclNode,
extendedType: extendedType,
conformanceList: conformanceList,
in: context
)
return expandedSources.map {
Expand Down
37 changes: 10 additions & 27 deletions Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
|| macro is MemberMacro.Type
|| macro is AccessorMacro.Type
|| macro is MemberAttributeMacro.Type
|| macro is ConformanceMacro.Type
|| macro is ExtensionMacro.Type)
}

Expand Down Expand Up @@ -213,7 +212,7 @@ class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {

if let declGroup = decl.asProtocol(DeclGroupSyntax.self) {
newItems.append(
contentsOf: expandConformances(of: declGroup).map {
contentsOf: expandExtensions(of: declGroup).map {
newDecl in CodeBlockItemSyntax(item: .decl(newDecl))
}
)
Expand Down Expand Up @@ -429,45 +428,29 @@ extension MacroApplication {
// If any of the custom attributes associated with the given declaration
// refer to conformance macros, expand them and return the resulting
// set of extension declarations.
private func expandConformances(of decl: DeclGroupSyntax) -> [DeclSyntax] {
let extendedType: Syntax
private func expandExtensions(of decl: DeclGroupSyntax) -> [DeclSyntax] {
let extendedType: TypeSyntax
if let identified = decl.asProtocol(IdentifiedDeclSyntax.self) {
extendedType = Syntax(identified.identifier.trimmed)
extendedType = "\(identified.identifier.trimmed)"
} else if let ext = decl.as(ExtensionDeclSyntax.self) {
extendedType = Syntax(ext.extendedType.trimmed)
extendedType = "\(ext.extendedType.trimmed)"
} else {
return []
}

var extensions: [DeclSyntax] = []
let macroAttributes = getMacroAttributes(attachedTo: decl.as(DeclSyntax.self)!, ofType: ConformanceMacro.Type.self)
for (attribute, conformanceMacro) in macroAttributes {
do {
let newConformances = try conformanceMacro.expansion(of: attribute, providingConformancesOf: decl, in: context)

for (type, whereClause) in newConformances {
var ext: DeclSyntax = """
extension \(extendedType): \(type) { }
"""
if let whereClause {
ext = DeclSyntax((ext.cast(ExtensionDeclSyntax.self)).with(\.genericWhereClause, whereClause))
}

extensions.append(DeclSyntax(ext))
}
} catch {
context.addDiagnostics(from: error, node: attribute)
}
}

let extensionMacroAttrs = getMacroAttributes(attachedTo: decl.as(DeclSyntax.self)!, ofType: ExtensionMacro.Type.self)
let extendedTypeSyntax = TypeSyntax("\(extendedType.trimmed)")
for (attribute, extensionMacro) in extensionMacroAttrs {
do {
// FIXME: We need a way for unit tests of extension macros to
// specify protocols already stated in source (e.g. as arguments
// to `assertMacroExpansion`).
let newExtensions = try extensionMacro.expansion(
of: attribute,
attachedTo: decl,
providingExtensionsOf: extendedTypeSyntax,
providingExtensionsOf: extendedType,
conformingTo: [],
in: context
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import SwiftSyntax

/// Describes a macro that can add conformances to the declaration it's
/// attached to.
public protocol ConformanceMacro: AttachedMacro {
public protocol ConformanceMacro: ExtensionMacro {
/// Expand an attached conformance macro to produce a set of conformances.
///
/// - Parameters:
Expand All @@ -31,3 +31,38 @@ public protocol ConformanceMacro: AttachedMacro {
in context: some MacroExpansionContext
) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)]
}

extension ConformanceMacro {
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {

let newConformances = try expansion(
of: node,
providingConformancesOf: declaration,
in: context
)

var extensions: [ExtensionDeclSyntax] = []
for (proto, whereClause) in newConformances {
let decl: DeclSyntax =
"""
extension \(type.trimmed): \(proto) {}
"""

var extensionDecl = decl.cast(ExtensionDeclSyntax.self)

if let whereClause {
extensionDecl = extensionDecl.with(\.genericWhereClause, whereClause)
}

extensions.append(extensionDecl)
}

return extensions
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public protocol ExtensionMacro: AttachedMacro {
/// - node: The custom attribute describing the attached macro.
/// - declaration: The declaration the macro attribute is attached to.
/// - type: The type to provide extensions of.
/// - protocols: The list of protocols to add conformances to. These will
/// always be protocols that `type` does not already state a conformance
/// to.
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: the set of extension declarations introduced by the macro,
Expand All @@ -30,6 +33,7 @@ public protocol ExtensionMacro: AttachedMacro {
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax]
}
43 changes: 16 additions & 27 deletions Sources/SwiftSyntaxMacros/MacroSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,12 @@ class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
return true
}

return !(macro is PeerMacro.Type || macro is MemberMacro.Type || macro is AccessorMacro.Type || macro is MemberAttributeMacro.Type || macro is ConformanceMacro.Type || macro is ExtensionMacro.Type)
return
!(macro is PeerMacro.Type
|| macro is MemberMacro.Type
|| macro is AccessorMacro.Type
|| macro is MemberAttributeMacro.Type
|| macro is ExtensionMacro.Type)
}

if newAttributes.isEmpty {
Expand Down Expand Up @@ -186,7 +191,7 @@ class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {

if let declGroup = decl.asProtocol(DeclGroupSyntax.self) {
newItems.append(
contentsOf: expandConformances(of: declGroup).map {
contentsOf: expandExtensions(of: declGroup).map {
newDecl in CodeBlockItemSyntax(item: .decl(newDecl))
}
)
Expand Down Expand Up @@ -402,45 +407,29 @@ extension MacroApplication {
// If any of the custom attributes associated with the given declaration
// refer to conformance macros, expand them and return the resulting
// set of extension declarations.
private func expandConformances(of decl: DeclGroupSyntax) -> [DeclSyntax] {
let extendedType: Syntax
private func expandExtensions(of decl: DeclGroupSyntax) -> [DeclSyntax] {
let extendedType: TypeSyntax
if let identified = decl.asProtocol(IdentifiedDeclSyntax.self) {
extendedType = Syntax(identified.identifier.trimmed)
extendedType = "\(identified.identifier.trimmed)"
} else if let ext = decl.as(ExtensionDeclSyntax.self) {
extendedType = Syntax(ext.extendedType.trimmed)
extendedType = "\(ext.extendedType.trimmed)"
} else {
return []
}

var extensions: [DeclSyntax] = []
let macroAttributes = getMacroAttributes(attachedTo: decl.as(DeclSyntax.self)!, ofType: ConformanceMacro.Type.self)
for (attribute, conformanceMacro) in macroAttributes {
do {
let newConformances = try conformanceMacro.expansion(of: attribute, providingConformancesOf: decl, in: context)

for (type, whereClause) in newConformances {
var ext: DeclSyntax = """
extension \(extendedType): \(type) { }
"""
if let whereClause {
ext = DeclSyntax((ext.cast(ExtensionDeclSyntax.self)).with(\.genericWhereClause, whereClause))
}

extensions.append(DeclSyntax(ext))
}
} catch {
context.addDiagnostics(from: error, node: attribute)
}
}

let extensionMacroAttrs = getMacroAttributes(attachedTo: decl.as(DeclSyntax.self)!, ofType: ExtensionMacro.Type.self)
let extendedTypeSyntax = TypeSyntax("\(extendedType.trimmed)")
for (attribute, extensionMacro) in extensionMacroAttrs {
do {
// FIXME: We need a way for unit tests of extension macros to
// specify protocols already stated in source (e.g. as arguments
// to `assertMacroExpansion`).
let newExtensions = try extensionMacro.expansion(
of: attribute,
attachedTo: decl,
providingExtensionsOf: extendedTypeSyntax,
providingExtensionsOf: extendedType,
conformingTo: [],
in: context
)

Expand Down
1 change: 1 addition & 0 deletions Tests/SwiftSyntaxMacrosTest/MacroSystemTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,7 @@ public struct SendableExtensionMacro: ExtensionMacro {
of node: AttributeSyntax,
attachedTo: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
let sendableExtension: DeclSyntax =
Expand Down
Loading