Skip to content

[Macros] Support code item macro expansion #1442

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 3 commits into from
Mar 28, 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
7 changes: 7 additions & 0 deletions Sources/SwiftCompilerPluginMessageHandling/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ extension CompilerPluginMessageHandler {
let rewritten = try _openExistential(macroSyntax, do: _expand)
expandedSource = CodeBlockItemListSyntax(rewritten.map { CodeBlockItemSyntax(item: .decl($0)) }).description

case let codeItemMacroDef as CodeItemMacro.Type:
func _expand<Node: FreestandingMacroExpansionSyntax>(node: Node) throws -> [CodeBlockItemSyntax] {
try codeItemMacroDef.expansion(of: node, in: context)
}
let rewritten = try _openExistential(macroSyntax, do: _expand)
expandedSource = CodeBlockItemListSyntax(rewritten).description

default:
throw MacroExpansionError.unmathedMacroRole
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,13 @@ internal enum PluginToHostMessage: Codable {

enum MacroRole: String, Codable {
case expression
case freeStandingDeclaration
case declaration
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm renaming this because macro roles typically don't include "freestanding" vs "attached". Does this need a corresponding change on the compiler side? If so I can revert this. @rintaro

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not need a corresponding change because I believe freeStandingDeclaration and expression aren't actually used in expandFreestandingMacro message. But yes, we should sync this file with swift repo one.
But you/we can do it later (after you merge this PR)

case accessor
case memberAttribute
case member
case peer
case conformance
case codeItem
}

struct SourceLocation: Codable {
Expand Down
8 changes: 7 additions & 1 deletion Sources/SwiftSyntaxMacros/MacroSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,13 @@ class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
let macro = macroSystem.macros[exprExpansion.macro.text]
{
do {
if let macro = macro as? DeclarationMacro.Type {
if let macro = macro as? CodeItemMacro.Type {
let expandedItemList = try macro.expansion(
of: exprExpansion,
in: context
)
newItems.append(contentsOf: expandedItemList)
} else if let macro = macro as? DeclarationMacro.Type {
let expandedItemList = try macro.expansion(
of: exprExpansion,
in: context
Expand Down
86 changes: 86 additions & 0 deletions Tests/SwiftSyntaxMacrosTest/MacroSystemTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,59 @@ extension CustomTypeWrapperMacro: AccessorMacro {
}
}

public struct UnwrapMacro: CodeItemMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> [CodeBlockItemSyntax] {
guard !node.argumentList.isEmpty else {
throw CustomError.message("'#unwrap' requires arguments")
}
let errorThrower = node.trailingClosure
let identifiers = try node.argumentList.map { argument in
guard let tupleElement = argument.as(TupleExprElementSyntax.self),
let identifierExpr = tupleElement.expression.as(IdentifierExprSyntax.self)
else {
throw CustomError.message("Arguments must be identifiers")
}
return identifierExpr.identifier
}

func elseBlock(_ token: TokenSyntax) -> CodeBlockSyntax {
let expr: ExprSyntax
if let errorThrower {
expr = """
\(errorThrower)("\(raw: token.text)")
"""
} else {
expr = """
fatalError("'\(raw: token.text)' is nil")
"""
}
return .init(
statements: .init([
.init(
leadingTrivia: " ",
item: .expr(expr),
trailingTrivia: " "
)
])
)
}

return identifiers.map { identifier in
CodeBlockItemSyntax(
item: CodeBlockItemSyntax.Item.stmt(
"""

guard let \(raw: identifier.text) else \(elseBlock(identifier))
"""
)
)
}
}
}

// MARK: Assertion helper functions

/// Assert that expanding the given macros in the original source produces
Expand Down Expand Up @@ -685,6 +738,7 @@ public let testMacros: [String: Macro.Type] = [
"wrapAllProperties": WrapAllProperties.self,
"wrapStoredProperties": WrapStoredProperties.self,
"customTypeWrapper": CustomTypeWrapperMacro.self,
"unwrap": UnwrapMacro.self,
]

final class MacroSystemTests: XCTestCase {
Expand Down Expand Up @@ -976,4 +1030,36 @@ final class MacroSystemTests: XCTestCase {
)

}

func testUnwrap() {
assertMacroExpansion(
macros: testMacros,
#"""
let x: Int? = 1
let y: Int? = nil
let z: Int? = 3
#unwrap(x, y, z)
#unwrap(x, y, z) {
fatalError("nil is \\($0)")
}
"""#,
#"""
let x: Int? = 1
let y: Int? = nil
let z: Int? = 3
guard let x else { fatalError("'x' is nil") }
guard let y else { fatalError("'y' is nil") }
guard let z else { fatalError("'z' is nil") }
guard let x else { {
fatalError("nil is \\($0)")
}("x") }
guard let y else { {
fatalError("nil is \\($0)")
}("y") }
guard let z else { {
fatalError("nil is \\($0)")
}("z") }
"""#
)
}
}