Indentation issue (again) with BodyMacro expansion

Despite reviewing some other discussions and (solved) bug reports concerning this problem (topic, issue, issue, topic) I am still not successful at relocating error messages to the right position, the column is still wrong. The goal of my code is to use the body of the function as a closure to an added function call, the code

@Step func step1(during execution: Execution) {
    print("hello")
}

becomes

func step1(during execution: Execution) {
    execution.effectuate(checking: StepID(crossModuleFileDesignation: #file, functionSignature: #function)) {
    print("hello")
    }
}

Displayed message are displayed in the right line, but the column is too far to the right, because an indentation is added during the expansion (despite the formatMode setting).

My macro implementation (repo):

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)
        }
    }
}

public struct StepMacro: 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 [
            """
            execution.effectuate("doing something in step1", checking: StepID(crossModuleFileDesignation: #file, functionSignature: #function)) {
            \(declaration.body!.statements, location: context.location(of: declaration.body!.statements, at: .beforeLeadingTrivia, filePathMode: .filePath), lineOffset: 1)
            }
            """
        ]
    }
    
}

Notes:

  • I find it quite useful to just add the textual additions to the code, not having to compose those statements in some complex manner.
  • The above insertion of the old body between the new parentheses does not use any indentation by itself, to not make the false localization of the error message even worse, but of course the following would be nice when looking at the expansion (with an indentation added):
...
        return [
            """
            execution.effectuate("doing something in step1", checking: StepID(crossModuleFileDesignation: #file, functionSignature: #function)) {
                \(declaration.body!.statements, location: context.location(of: declaration.body!.statements, at: .beforeLeadingTrivia, filePathMode: .filePath), lineOffset: 1)
            }
            """
        ]
...

Thank you for your help!

You are using declaration.body!.statements from the original code. Its indentation there is exactly the indentation applied when you insert the statements into the code string in the macro implementation. That’s why indentation with 4 spaces is maintained and indenting the line in the string makes it even look worse.

In order to add another level of indentation, you need to iterate over all statements and add the appropriate amount of leading trivia to each node recursively. For SwiftLint, I added CodeIndentingRewriter at that time to do exactly that. It works for the use cases we have there but might not yet cover every kind of code properly. Perhaps it could be of help for you.

As this problem happens to be coming up again and again, I may think of contributing the rewriter to the SwiftSyntax repository or any other general place that would be a good fit. :thinking:

1 Like

Thank you for the help, but I have no idea how to apply that despite looking at your code. I have node.arguments, according to what you have written I have to change those, how do I do this?

I also wonder why this is so complicated, that should be a common case that the macro API should be able to handle via some easier code. And does it still perform well, because this obviously is something that has to run each time I edit the function body?

(The current code I am currently working with is PipelineStepMacro with an example in the tests.)

BTW I get multiple warnings 'StepMacros' is missing a dependency on 'SwiftBasicFormat' because dependency scan of Swift module 'StepMacros' discovered a dependency on 'SwiftBasicFormat' when I first run the tests, on a second run those warnings disappear. Is there something wrong with Package.swift or is this a Swift problem or an Xcode problem?

OK, after reviewing the forums topic Indentation issue with BodyMacro expansion I got the idea...

let newBody = CodeIndentingRewriter(style: .unindentSpaces(4)).rewrite(body).as(CodeBlockSyntax.self)

and then using newBody ?? body in what follows.

See the implementation in the PipelineStepMacro (to be moved into the Pipeline package if proven in practice).

Thanks again!

Yes, that's it. You can also use ....rewrite(body).cast(CodeBlockSyntax.self) to avoid the ?? body since you know what you are dealing with.

1 Like

I just found Indenter in the SwiftBasicFormat module. It should do the job as well and can be easily added as a dependency (perhaps that would even resolve the Xcode issue you mentioned above).

But while properly formatted code is nice to have for inspection, it's not necessary to implement a macro. The Swift compiler doesn't care at all.

1 Like