swiftlang/swift-syntax

ExpressionMacro trailing closure inherits code's original indentation

Opened this issue · 2 comments

Description

If macro is nested in some closure, its trailing closure preserves the original code indentation. So if I build FunctionCallExprSyntax with trailing closure that takes statements from the original trailing closure, the new statements are incorrectly indented.

This probably applies as well to closure as last argument.

Is there any manual way to trim the statements to decrease the indent?

Steps to Reproduce

assertMacroExpansion(
    #"""
    VStack {
        #SomeMacro(arg1) { output in
            EmptyView()
        }
    }
    """#,
    expandedSource: #"""
    VStack {
        SomeWrapper(arg1) { output in
            EmptyView()
        }
    }
    """#,
    macros: testMacros
)

The code above incorrectly expands to:

    #"""
    VStack {
        SomeWrapper(arg1) { output in
                EmptyView()
        }
    }
    """#

Tracked in Apple’s issue tracker as rdar://114755235

Here’s a full test case that reproduces the issue.

func testMultilineIndentedMacroExpression() {
  struct TestMacro: ExpressionMacro {
    static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> ExprSyntax {
      return ExprSyntax(FunctionCallExprSyntax(
        calledExpression: ExprSyntax("Test"),
        leftParen: node.leftParen,
        arguments: node.argumentList,
        rightParen: node.rightParen,
        trailingClosure: node.trailingClosure,
        additionalTrailingClosures: node.additionalTrailingClosures
      ))
    }
  }

  assertMacroExpansion(
    #"""
    func test() {
      #Test() { output in
        EmptyView()
      }
    }
    """#,
    expandedSource: #"""
    func test() {
      Test() { output in
        EmptyView()
      }
    }
    """#,
    macros: ["Test": TestMacro.self]
  )
}

Fixing this is very tricky or even impossible.

We add the indentation level of the macro expression to all lines of the expanded macro expression so that if you have a macro that expands #Test to

Test {
  print("a")
}

and you us #Test in an indented context like

func test() {
  #Test
}

Then the expanded source should add another two spaces even to the print line so that it’s indented by 4 spaces in the end. And distinguishing between this case and the case described in the issue is impossible in general.

The other alternative to fixing this could be that we strip the indentation of the line that the macro starts at from the syntax tree that we pass to the expansion function but that doesn’t match what the compiler is doing and the idea is that the macro receives the syntax node of the macro as-is.