Update blankLinesAtStartOfScope rule to support switch cases and closures with captures/params (#1997)

This commit is contained in:
Cal Stephens
2025-05-12 20:18:43 -07:00
committed by Cal Stephens
parent 94b850cac0
commit 2fd589f4a0
5 changed files with 110 additions and 9 deletions
-1
View File
@@ -191,7 +191,6 @@ extension Declaration {
switch keyword {
// Properties and property-like declarations
case "let", "var", "operator", "precedencegroup":
if isOverriddenDeclaration, availableTypes.contains(.overriddenProperty) {
return .overriddenProperty
}
+20 -6
View File
@@ -9,17 +9,14 @@
import Foundation
public extension FormatRule {
/// Remove blank lines immediately after an opening brace, bracket, paren or chevron
/// Remove blank lines immediately after an opening brace, bracket, paren, chevron, or colon
/// that starts a new scope.
static let blankLinesAtStartOfScope = FormatRule(
help: "Remove leading blank line at the start of a scope.",
options: ["typeblanklines"]
) { formatter in
formatter.forEach(.startOfScope) { i, token in
guard ["{", "(", "[", "<"].contains(token.string),
let indexOfFirstLineBreak = formatter.index(of: .nonSpaceOrComment, after: i),
// If there is extra code on the same line, ignore it
formatter.tokens[indexOfFirstLineBreak].isLinebreak
else { return }
guard ["{", "(", "[", "<", ":"].contains(token.string) else { return }
// Consumers can choose whether or not this rule should apply to type bodies
if !formatter.options.removeStartOrEndBlankLinesFromTypes,
@@ -29,6 +26,23 @@ public extension FormatRule {
return
}
// If this is a closure with captures or params, skip to the `in` keyword
// before we look for a blank line at the start of the scope.
var startOfScope = i
if formatter.isStartOfClosure(at: startOfScope),
let endOfScope = formatter.endOfScope(at: startOfScope),
startOfScope + 1 < endOfScope,
let inKeywordIndex = formatter.index(of: .keyword("in"), in: startOfScope + 1 ..< endOfScope),
formatter.startOfScope(at: inKeywordIndex) == startOfScope
{
startOfScope = inKeywordIndex
}
guard let indexOfFirstLineBreak = formatter.index(of: .nonSpaceOrComment, after: startOfScope),
// If there is extra code on the same line, ignore it
formatter.tokens[indexOfFirstLineBreak].isLinebreak
else { return }
// Find next non-space token
var index = indexOfFirstLineBreak + 1
var indexOfLastLineBreak = indexOfFirstLineBreak
@@ -78,7 +78,6 @@ public extension FormatRule {
}
extensionDeclaration.body.forEachRecursiveDeclarationExcludingTypeBodies { bodyDeclaration in
let visibility = bodyDeclaration.visibility()
if memberVisibility > visibility ?? extensionVisibility ?? .internal {
if visibility == nil {
-1
View File
@@ -24,7 +24,6 @@ public extension FormatRule {
|| $0.isDeclarationTypeKeyword(including: Array(Token.swiftTypeKeywords))
}
) { index, token in
let rangeToSort: ClosedRange<Int>
let numberOfLeadingLinebreaks: Int
@@ -118,4 +118,94 @@ class BlankLinesAtStartOfScopeTests: XCTestCase {
"""
XCTAssertEqual(try lint(input, rules: [.blankLinesAtStartOfScope, .organizeDeclarations]), [])
}
func testRemovesBlankLineFromStartOfSwitchCase() {
let input = """
switch bool {
case true:
print("true")
case false:
print("false")
}
"""
let output = """
switch bool {
case true:
print("true")
case false:
print("false")
}
"""
testFormatting(for: input, output, rule: .blankLinesAtStartOfScope)
}
func testRemovesBlankLineInClosureWithParams() {
let input = """
presenter.present(viewController, animated: animated) { animated in
if animated {
self?.completion()
}
}
"""
let output = """
presenter.present(viewController, animated: animated) { animated in
if animated {
self?.completion()
}
}
"""
testFormatting(for: input, output, rule: .blankLinesAtStartOfScope)
}
func testRemovesBlankLineInClosureWithCapture() {
let input = """
presenter.present(viewController, animated: animated) { [weak self] animated in
if animated {
self?.completion()
}
}
"""
let output = """
presenter.present(viewController, animated: animated) { [weak self] animated in
if animated {
self?.completion()
}
}
"""
testFormatting(for: input, output, rule: .blankLinesAtStartOfScope)
}
func testRemovesBlankLineInClosureWithActorAnnotion() {
let input = """
presenter.present(viewController, animated: animated) { @MainActor in
if animated {
self?.completion()
}
}
"""
let output = """
presenter.present(viewController, animated: animated) { @MainActor in
if animated {
self?.completion()
}
}
"""
testFormatting(for: input, output, rule: .blankLinesAtStartOfScope)
}
}