From 2fd589f4a08763fc1a7ebb2ded4e2a05b8d982d6 Mon Sep 17 00:00:00 2001 From: Cal Stephens Date: Thu, 3 Apr 2025 09:14:40 -0700 Subject: [PATCH] Update `blankLinesAtStartOfScope` rule to support switch cases and closures with captures/params (#1997) --- Sources/DeclarationType.swift | 1 - Sources/Rules/BlankLinesAtStartOfScope.swift | 26 ++++-- Sources/Rules/ExtensionAccessControl.swift | 1 - Sources/Rules/SortDeclarations.swift | 1 - .../Rules/BlankLinesAtStartOfScopeTests.swift | 90 +++++++++++++++++++ 5 files changed, 110 insertions(+), 9 deletions(-) diff --git a/Sources/DeclarationType.swift b/Sources/DeclarationType.swift index 76cf7a9c..77a5f707 100644 --- a/Sources/DeclarationType.swift +++ b/Sources/DeclarationType.swift @@ -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 } diff --git a/Sources/Rules/BlankLinesAtStartOfScope.swift b/Sources/Rules/BlankLinesAtStartOfScope.swift index ba34f9ce..97944cc6 100644 --- a/Sources/Rules/BlankLinesAtStartOfScope.swift +++ b/Sources/Rules/BlankLinesAtStartOfScope.swift @@ -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 diff --git a/Sources/Rules/ExtensionAccessControl.swift b/Sources/Rules/ExtensionAccessControl.swift index 60a361f1..c8f61358 100644 --- a/Sources/Rules/ExtensionAccessControl.swift +++ b/Sources/Rules/ExtensionAccessControl.swift @@ -78,7 +78,6 @@ public extension FormatRule { } extensionDeclaration.body.forEachRecursiveDeclarationExcludingTypeBodies { bodyDeclaration in - let visibility = bodyDeclaration.visibility() if memberVisibility > visibility ?? extensionVisibility ?? .internal { if visibility == nil { diff --git a/Sources/Rules/SortDeclarations.swift b/Sources/Rules/SortDeclarations.swift index 27591b95..5a8dbbe3 100644 --- a/Sources/Rules/SortDeclarations.swift +++ b/Sources/Rules/SortDeclarations.swift @@ -24,7 +24,6 @@ public extension FormatRule { || $0.isDeclarationTypeKeyword(including: Array(Token.swiftTypeKeywords)) } ) { index, token in - let rangeToSort: ClosedRange let numberOfLeadingLinebreaks: Int diff --git a/Tests/Rules/BlankLinesAtStartOfScopeTests.swift b/Tests/Rules/BlankLinesAtStartOfScopeTests.swift index fd33972d..df87fc5d 100644 --- a/Tests/Rules/BlankLinesAtStartOfScopeTests.swift +++ b/Tests/Rules/BlankLinesAtStartOfScopeTests.swift @@ -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) + } }