From 3d1e03b9d48426f5aa50f442e84a580a34c11716 Mon Sep 17 00:00:00 2001 From: TTOzzi Date: Sat, 4 Jan 2025 22:34:44 +0900 Subject: [PATCH] Fix NoEmptyLinesOpeningClosingBraces to respect consecutive newlines when a function starts or ends with a comment --- .../NoEmptyLineOpeningClosingBraces.swift | 43 +++++++++++--- ...oEmptyLinesOpeningClosingBracesTests.swift | 56 +++++++++++++++++++ 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/Sources/SwiftFormat/Rules/NoEmptyLineOpeningClosingBraces.swift b/Sources/SwiftFormat/Rules/NoEmptyLineOpeningClosingBraces.swift index b01d173bf..5158dbb95 100644 --- a/Sources/SwiftFormat/Rules/NoEmptyLineOpeningClosingBraces.swift +++ b/Sources/SwiftFormat/Rules/NoEmptyLineOpeningClosingBraces.swift @@ -69,7 +69,9 @@ public final class NoEmptyLinesOpeningClosingBraces: SyntaxFormatRule { } func rewritten(_ token: TokenSyntax) -> TokenSyntax { - let (trimmedLeadingTrivia, count) = token.leadingTrivia.trimmingSuperfluousNewlines() + let (trimmedLeadingTrivia, count) = token.leadingTrivia.trimmingSuperfluousNewlines( + fromClosingBrace: token.tokenKind == .rightBrace + ) if trimmedLeadingTrivia.sourceLength != token.leadingTriviaLength { diagnose(.removeEmptyLinesBefore(count), on: token, anchor: .start) return token.with(\.leadingTrivia, trimmedLeadingTrivia) @@ -83,7 +85,7 @@ public final class NoEmptyLinesOpeningClosingBraces: SyntaxFormatRule { if let first = collection.first, first.leadingTrivia.containsNewlines, let index = collection.index(of: first) { - let (trimmedLeadingTrivia, count) = first.leadingTrivia.trimmingSuperfluousNewlines() + let (trimmedLeadingTrivia, count) = first.leadingTrivia.trimmingSuperfluousNewlines(fromClosingBrace: false) if trimmedLeadingTrivia.sourceLength != first.leadingTriviaLength { diagnose(.removeEmptyLinesAfter(count), on: first, anchor: .leadingTrivia(0)) var first = first @@ -96,24 +98,49 @@ public final class NoEmptyLinesOpeningClosingBraces: SyntaxFormatRule { } extension Trivia { - func trimmingSuperfluousNewlines() -> (Trivia, Int) { + func trimmingSuperfluousNewlines(fromClosingBrace: Bool) -> (Trivia, Int) { var trimmmed = 0 + var pendingNewlineCount = 0 let pieces = self.indices.reduce([TriviaPiece]()) { (partialResult, index) in let piece = self[index] // Collapse consecutive newlines into a single one if case .newlines(let count) = piece { - if let last = partialResult.last, last.isNewline { - trimmmed += count - return partialResult + if fromClosingBrace { + if index == self.count - 1 { + // For the last index(newline right before the closing brace), collapse into a single newline + trimmmed += count - 1 + return partialResult + [.newlines(1)] + } else { + pendingNewlineCount += count + return partialResult + } } else { - trimmmed += count - 1 - return partialResult + [.newlines(1)] + if let last = partialResult.last, last.isNewline { + trimmmed += count + return partialResult + } else if index == 0 { + // For leading trivia not associated with a closing brace, collapse the first newline into a single one + trimmmed += count - 1 + return partialResult + [.newlines(1)] + } else { + return partialResult + [piece] + } } } // Remove spaces/tabs surrounded by newlines if piece.isSpaceOrTab, index > 0, index < self.count - 1, self[index - 1].isNewline, self[index + 1].isNewline { return partialResult } + // Handle pending newlines if there are any + if pendingNewlineCount > 0 { + if index < self.count - 1 { + let newlines = TriviaPiece.newlines(pendingNewlineCount) + pendingNewlineCount = 0 + return partialResult + [newlines] + [piece] + } else { + return partialResult + [.newlines(1)] + [piece] + } + } // Retain other trivia pieces return partialResult + [piece] } diff --git a/Tests/SwiftFormatTests/Rules/NoEmptyLinesOpeningClosingBracesTests.swift b/Tests/SwiftFormatTests/Rules/NoEmptyLinesOpeningClosingBracesTests.swift index 30f9471fc..bb555739a 100644 --- a/Tests/SwiftFormatTests/Rules/NoEmptyLinesOpeningClosingBracesTests.swift +++ b/Tests/SwiftFormatTests/Rules/NoEmptyLinesOpeningClosingBracesTests.swift @@ -136,4 +136,60 @@ final class NoEmptyLinesOpeningClosingBracesTests: LintOrFormatRuleTestCase { ] ) } + + func testNoEmptyLinesOpeningClosingBracesInFunctionBeginningAndEndingWithComment() { + assertFormatting( + NoEmptyLinesOpeningClosingBraces.self, + input: """ + func myFunc() { + // Some comment here + + // Do a thing + var x = doAThing() + + // Do a thing + + var y = doAThing() + + // Some other comment here + } + """, + expected: """ + func myFunc() { + // Some comment here + + // Do a thing + var x = doAThing() + + // Do a thing + + var y = doAThing() + + // Some other comment here + } + """ + ) + } + + func testNoEmptyLinesOpeningClosingBracesInFunctionWithEmptyLinesOnly() { + assertFormatting( + NoEmptyLinesOpeningClosingBraces.self, + input: """ + func myFunc() { + + + + + + 1️⃣} + """, + expected: """ + func myFunc() { + } + """, + findings: [ + FindingSpec("1️⃣", message: "remove empty lines before '}'") + ] + ) + } }