diff --git a/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift b/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift index d3b356106c5..00c4369add1 100644 --- a/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift +++ b/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift @@ -527,6 +527,16 @@ public class AttributeRemover: SyntaxRewriter { var triviaToAttachToNextToken: Trivia = Trivia() + /// When `true`, the next call to `prependAndClearAccumulatedTrivia(to:)` + /// should also drop one leading newline (and any spaces/tabs that + /// immediately precede it) from the resulting trivia. + /// + /// This is set when we remove an attribute that occupies its own line but + /// whose own leading trivia does not contain the line-terminating newline + /// (e.g. the attribute is the first token of the file, so the newline + /// terminating its line lives on the following node). + var dropLeadingNewlineOnNextNode: Bool = false + /// Initializes an attribute remover with a given predicate to determine which attributes to remove. /// /// - Parameter predicate: A closure that determines whether a given `AttributeSyntax` should be removed. @@ -560,6 +570,16 @@ public class AttributeRemover: SyntaxRewriter { !nextToken.leadingTrivia.isEmpty { leadingTrivia = Trivia(pieces: leadingTrivia.pieces[..(to syntaxNode: T) -> T { - guard !triviaToAttachToNextToken.isEmpty else { return syntaxNode } - defer { triviaToAttachToNextToken = Trivia() } - return syntaxNode.with(\.leadingTrivia, triviaToAttachToNextToken + syntaxNode.leadingTrivia) + guard !triviaToAttachToNextToken.isEmpty || dropLeadingNewlineOnNextNode else { + return syntaxNode + } + defer { + triviaToAttachToNextToken = Trivia() + dropLeadingNewlineOnNextNode = false + } + + var combined = triviaToAttachToNextToken + syntaxNode.leadingTrivia + if dropLeadingNewlineOnNextNode, + let firstNewline = combined.pieces.firstIndex(where: \.isNewline), + combined.pieces[..