Skip to content
Open
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
44 changes: 41 additions & 3 deletions Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -560,6 +570,16 @@ public class AttributeRemover: SyntaxRewriter {
!nextToken.leadingTrivia.isEmpty
{
leadingTrivia = Trivia(pieces: leadingTrivia.pieces[..<lastNewline])
} else if leadingTrivia.isEmpty,
attribute.trailingTrivia.allSatisfy(\.isSpaceOrTab),
attribute.previousToken(viewMode: .sourceAccurate)
.map({ $0.trailingTrivia.pieces.last?.isNewline ?? false }) ?? true
{
// The attribute occupies its own line, but the newline terminating that
// line lives on a following node (either the next attribute or the next
// token on a following node) rather than on this attribute. Record that
// we need to drop that newline when we attach accumulated trivia.
dropLeadingNewlineOnNextNode = true
}

// Drop any spaces or tabs from the trailing trivia because there’s no
Expand Down Expand Up @@ -609,12 +629,30 @@ public class AttributeRemover: SyntaxRewriter {
/// significant trivia accumulated from removed attributes to the provided subsequent node.
/// Once attached, the accumulated trivia is cleared.
///
/// If `dropLeadingNewlineOnNextNode` is `true`, this function additionally
/// drops one leading newline (and any spaces or tabs that precede it) from
/// the combined trivia, removing the line vacated by a removed attribute
/// whose own leading trivia did not include that newline.
///
/// - Parameter node: The syntax node receiving the accumulated trivia.
/// - Returns: The modified syntax node with the prepended trivia.
private func prependAndClearAccumulatedTrivia<T: SyntaxProtocol>(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[..<firstNewline].allSatisfy(\.isSpaceOrTab)
{
combined = Trivia(pieces: combined.pieces[combined.index(after: firstNewline)...])
}
return syntaxNode.with(\.leadingTrivia, combined)
}
}

Expand Down
15 changes: 5 additions & 10 deletions Tests/SwiftSyntaxMacroExpansionTest/AttributeRemoverTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,25 +59,23 @@ final class AttributeRemoverTests: XCTestCase {
)
}

// FIXME: `AttributeRemover` should not leave a leading newline.
func testEmptyOnOwnLineBeforeVariable() {
assertSyntaxRemovingTestAttributes(
"""
@Test
var x: Int
""",
reduction: "\nvar x: Int"
reduction: "var x: Int"
)
}

// FIXME: `AttributeRemover` should not leave a leading newline.
func testEmptyTwiceOnOwnLineBeforeVariable() {
assertSyntaxRemovingTestAttributes(
"""
@Test @Test
var x: Int
""",
reduction: "\nvar x: Int"
reduction: "var x: Int"
)
}

Expand Down Expand Up @@ -166,7 +164,6 @@ final class AttributeRemoverTests: XCTestCase {
)
}

// FIXME: `AttributeRemover` should not leave a leading newline.
func testEmptyNewlineBlockComment() {
assertSyntaxRemovingTestAttributes(
"""
Expand All @@ -175,7 +172,7 @@ final class AttributeRemoverTests: XCTestCase {
var value: Int
""",
reduction: """
\n/* comment */
/* comment */
var value: Int
"""
)
Expand Down Expand Up @@ -339,7 +336,6 @@ final class AttributeRemoverTests: XCTestCase {
)
}

// FIXME: `AttributeRemover` should not leave a leading newline.
func testEmptyAndAttributeOnOwnLinesBeforeVariable() {
assertSyntaxRemovingTestAttributes(
"""
Expand All @@ -348,7 +344,7 @@ final class AttributeRemoverTests: XCTestCase {
var x: Int
""",
reduction: """
\n@State
@State
var x: Int
"""
)
Expand All @@ -367,14 +363,13 @@ final class AttributeRemoverTests: XCTestCase {
)
}

// FIXME: `AttributeRemover` should not leave a leading newline.
func testEmptyOnOwnLineThenEmptyBeforeVariable() {
assertSyntaxRemovingTestAttributes(
"""
@Test
@Test var x: Int
""",
reduction: "\nvar x: Int"
reduction: "var x: Int"
)
}

Expand Down