Usability of function body macros

I'm trying out an idea for a function body macro, and running into some usability issues that essentially stem from the fact that you now have a significant amount of code - the entire body of the function - that is not the code that will be complied and run, no matter how little your macro transforms the body.

The result is that, in Xcode, you have to expand the macro in order to see where syntax errors or test failures are, or use breakpoints. This can be pretty awkward, especially since Xcode doesn't update the expansion live as you edit the code. And even if it did, it would still be awkward. But the macro is transforming your code after all, so I don't know how these things can be mitigated.

Has anyone else found this to be an issue? I'm starting to think about trying an alternate solution that might be easier to use.

2 Likes

Yes! And breakpoints only seem to work in the expanded code, unless I'm doing something wrong. I just opened a thread here: Debug Code wrapped by Macro

For this you can use #sourceLocation to move the transformed pieces of code to their original location. I use the following extension for convenience:

extension SyntaxStringInterpolation {
    mutating func appendInterpolation<Node: SyntaxProtocol>(
        _ node: Node,
        location: AbstractSourceLocation?,
        lineOffset: Int? = nil,
        close: Bool = true
    ) {
        if let location {
            let line = if let lineOffset {
                ExprSyntax("\(literal: Int(location.line.as(IntegerLiteralExprSyntax.self)?.literal.text ?? "0")! + lineOffset)")
            } else {
                location.line
            }
            var block = CodeBlockItemListSyntax {
                "#sourceLocation(file: \(location.file), line: \(line))"
                "\(node)"
            }
            if close {
                block.append("\n#sourceLocation()")
            }
            appendInterpolation(block)
        } else {
            appendInterpolation(node)
        }
    }
}

It can be tricky to move to the correct column in some situations and requires to set the macro formatMode to .disabled:

public struct SomeBodyMacro: BodyMacro {
    
    public static var formatMode: FormatMode { .disabled }
    
    public static func expansion(
        of node: AttributeSyntax,
        providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax,
        in context: some MacroExpansionContext
    ) throws -> [CodeBlockItemSyntax] {
        return [
            """
            // 
            // 
            //
            \(declaration.body!.statements, location: context.location(of: declaration.body!.statements, filePathMode: .filePath), lineOffset: 0)
            """
        ]
    }
    
}

@SomeBodyMacro
func test() {
    let a: Int = "" // ❌ Cannot convert value of type 'String' to specified type 'Int'
}
// EXPANSION:
{
//
//
//
#sourceLocation(file: <FILE PATH>, line: <LINE>)
    let a: Int = ""
#sourceLocation()
}

I don't think there's any way around that because the macro is allowed to produce anything at all. There's no requirement, guarantee, or even a way to indicate that any of the original source will be carried over. The only safe assumption for Xcode to make is that the original body is just ignored.