Skip to content

Fix missing newline in member macro #2320

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ final class AddAsyncMacroTests: XCTestCase {
}
}
}

}
"""#,
macros: macros,
Expand Down Expand Up @@ -70,6 +71,7 @@ final class AddAsyncMacroTests: XCTestCase {
continuation.resume(returning: returnValue)
}
}

}
""",
macros: macros,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ final class AddCompletionHandlerMacroTests: XCTestCase {
Task {
completionHandler(await f(a: a, for: b, value))
}

}
""",
macros: macros,
Expand Down
32 changes: 30 additions & 2 deletions Sources/SwiftParser/ParseSourceFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,9 @@ extension Parser {
}

let remainingTokens = self.consumeRemainingTokens()

if remainingTokens.isEmpty {
return into
return R.init(transferTrailingTrivaFromEndOfFileIfPresent(raw: into.raw))!
}

let existingUnexpected: [RawSyntax]
Expand All @@ -166,6 +167,33 @@ extension Parser {
let unexpected = RawUnexpectedNodesSyntax(elements: existingUnexpected + remainingTokens, arena: self.arena)

let withUnexpected = layout.replacingChild(at: layout.children.count - 1, with: unexpected.raw, arena: self.arena)
return R.init(withUnexpected)!

return R.init(transferTrailingTrivaFromEndOfFileIfPresent(raw: withUnexpected))!
}

/// Parses the end-of-file token and appends its leading trivia to the provided `RawSyntax`.
/// - Parameter raw: The raw syntax node to which the leading trivia of the end-of-file token will be appended.
/// - Returns: A new `RawSyntax` instance with trailing trivia transferred from the end-of-file token if present, otherwise it will return the raw parameter..
private mutating func transferTrailingTrivaFromEndOfFileIfPresent(raw: RawSyntax) -> RawSyntax {
guard let endOfFileToken = self.consume(if: .endOfFile),
!endOfFileToken.leadingTriviaPieces.isEmpty,
let raw = raw.withTrailingTrivia(
Trivia(
rawPieces: (raw.trailingTriviaPieces ?? []) + endOfFileToken.leadingTriviaPieces
),
arena: self.arena
)
else {
return raw
}

return raw
}
}

private extension Trivia {
init(rawPieces: [RawTriviaPiece]) {
let pieces = rawPieces.map(TriviaPiece.init(raw:))
self.init(pieces: pieces)
}
}
6 changes: 4 additions & 2 deletions Sources/SwiftSyntax/Raw/RawSyntax.swift
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,8 @@ extension RawSyntax {
/// - Parameters:
/// - leadingTrivia: The trivia to attach.
/// - arena: SyntaxArena to the result node data resides.
func withLeadingTrivia(_ leadingTrivia: Trivia, arena: SyntaxArena) -> RawSyntax? {
@_spi(RawSyntax)
public func withLeadingTrivia(_ leadingTrivia: Trivia, arena: SyntaxArena) -> RawSyntax? {
switch view {
case .token(let tokenView):
return .makeMaterializedToken(
Expand All @@ -328,7 +329,8 @@ extension RawSyntax {
/// - Parameters:
/// - trailingTrivia: The trivia to attach.
/// - arena: SyntaxArena to the result node data resides.
func withTrailingTrivia(_ trailingTrivia: Trivia, arena: SyntaxArena) -> RawSyntax? {
@_spi(RawSyntax)
public func withTrailingTrivia(_ trailingTrivia: Trivia, arena: SyntaxArena) -> RawSyntax? {
switch view {
case .token(let tokenView):
return .makeMaterializedToken(
Expand Down
30 changes: 25 additions & 5 deletions Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -762,10 +762,8 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
return CodeBlockItemListSyntax(newItems)
}

override func visit(_ node: MemberBlockItemListSyntax) -> MemberBlockItemListSyntax {
override func visit(_ node: MemberBlockSyntax) -> MemberBlockSyntax {
let parentDeclGroup = node
.parent?
.as(MemberBlockSyntax.self)?
.parent?
.as(DeclSyntax.self)
var newItems: [MemberBlockItemSyntax] = []
Expand All @@ -792,7 +790,7 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
extensions += expandExtensions(of: node.decl)
}

for var item in node {
for var item in node.members {
// Expand member attribute members attached to the declaration context.
// Note that MemberAttribute macros are _not_ applied to generated members
if let parentDeclGroup, let decl = item.decl.asProtocol(WithAttributesSyntax.self) {
Expand Down Expand Up @@ -825,7 +823,29 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
}
}

return .init(newItems)
/// Returns an leading trivia for the member blocks closing brace.
/// It will add a leading newline, if there is none.
var leadingTriviaForClosingBrace: Trivia {
if newItems.isEmpty {
return node.rightBrace.leadingTrivia
}

if node.rightBrace.leadingTrivia.contains(where: { $0.isNewline }) {
return node.rightBrace.leadingTrivia
}

if newItems.last?.trailingTrivia.pieces.last?.isNewline ?? false {
return node.rightBrace.leadingTrivia
} else {
return .newline + node.rightBrace.leadingTrivia
}
}

return MemberBlockSyntax(
leftBrace: node.leftBrace,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ahoppen added leftBrace: node.leftBrace, since your review.

If not, it will fail testAssertMacroExpansionIgnoresHighlightMatchingIfNil where "struct S { }" -> "struct S {}"

members: MemberBlockItemListSyntax(newItems),
rightBrace: node.rightBrace.with(\.leadingTrivia, leadingTriviaForClosingBrace)
)
}

override func visit(_ node: VariableDeclSyntax) -> DeclSyntax {
Expand Down
1 change: 1 addition & 0 deletions Tests/SwiftBasicFormatTest/BasicFormatTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,7 @@ final class BasicFormatTest: XCTestCase {
func test() {
Task {
}

}
"""
)
Expand Down
13 changes: 13 additions & 0 deletions Tests/SwiftParserTest/DeclarationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3141,4 +3141,17 @@ final class DeclarationTests: ParserTestCase {
experimentalFeatures: .nonescapableTypes
)
}

func testDeclarationEndingWithNewline() {
let inputs: [UInt: String] = [
#line: "var x = 0\n",
#line: "var x = 0 garbage\n",
#line: "var x = 0 \n",
]

for (line, input) in inputs {
let decl = DeclSyntax(stringLiteral: input)
XCTAssertEqual(decl.description, input, line: line)
}
}
}
125 changes: 125 additions & 0 deletions Tests/SwiftSyntaxMacroExpansionTest/MemberMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -299,4 +299,129 @@ final class MemberMacroTests: XCTestCase {
]
)
}

func testAddMemberToEmptyDeclaration() {
struct TestMacro: MemberMacro {
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
return [DeclSyntax("var x = 0")]
}
}

assertMacroExpansion(
"""
@Test
struct Foo {}
""",
expandedSource: """
struct Foo {
var x = 0
}
""",
macros: [
"Test": TestMacro.self
],
indentationWidth: indentationWidth
)
}

func testAddTwoMembersToEmptyDeclaration() {
struct TestMacro: MemberMacro {
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
return [DeclSyntax("var x = 0"), DeclSyntax("var x = 0")]
}
}

assertMacroExpansion(
"""
@Test
struct Foo {}
""",
expandedSource: """
struct Foo {
var x = 0
var x = 0
}
""",
macros: [
"Test": TestMacro.self
],
indentationWidth: indentationWidth
)
}

func testAddMemberToEmptyDeclarationWithEndingNewline() {
struct TestMacro: MemberMacro {
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
return [DeclSyntax("var x = 0\n")]
}
}

assertMacroExpansion(
"""
@Test
struct Foo {}
""",
expandedSource: """
struct Foo {
var x = 0
}
""",
macros: [
"Test": TestMacro.self
],
indentationWidth: indentationWidth
)
}

func testAddMemberToDeclarationWithASingleVariable() {
struct TestMacro: MemberMacro {
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
return [DeclSyntax("var x = 0\n")]
}
}

assertMacroExpansion(
"""
@Test
struct Foo {
var y = 0
}
""",
expandedSource: """
struct Foo {
var y = 0
var x = 0
}
""",
macros: [
"Test": TestMacro.self
],
indentationWidth: indentationWidth
)
}
}