Skip to content

Commit 0c71671

Browse files
Add NoEmptyLinesOpeningClosingBraces rule
> This rule removes empty lines after opening braces and before closing braces: > > ```swift > // original > > struct P { > > let x: Int > > let y: Int > > } > > // formatted > > struct P { > let x: Int > > let y: Int > } > ``` > > That's similar to [vertical_whitespace_opening_braces](https://realm.github.io/SwiftLint/vertical_whitespace_opening_braces.html) and [vertical_whitespace_closing_braces](https://realm.github.io/SwiftLint/vertical_whitespace_opening_braces.html). > > It's a style used in official swift projects such as **swift-syntax** and **sourcekit-lsp**.
1 parent 89ccc79 commit 0c71671

7 files changed

+304
-0
lines changed

Documentation/RuleDocumentation.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Here's the list of available rules:
2929
- [NoAssignmentInExpressions](#NoAssignmentInExpressions)
3030
- [NoBlockComments](#NoBlockComments)
3131
- [NoCasesWithOnlyFallthrough](#NoCasesWithOnlyFallthrough)
32+
- [NoEmptyLinesOpeningClosingBraces](#NoEmptyLinesOpeningClosingBraces)
3233
- [NoEmptyTrailingClosureParentheses](#NoEmptyTrailingClosureParentheses)
3334
- [NoLabelsInCasePatterns](#NoLabelsInCasePatterns)
3435
- [NoLeadingUnderscores](#NoLeadingUnderscores)
@@ -271,6 +272,16 @@ Format: The fallthrough `case` is added as a prefix to the next case unless the
271272

272273
`NoCasesWithOnlyFallthrough` rule can format your code automatically.
273274

275+
### NoEmptyLinesOpeningClosingBraces
276+
277+
Empty lines are forbidden after opening braces and before closing braces.
278+
279+
Lint: Empty lines after opening braces and before closing braces yield a lint error.
280+
281+
Format: Empty lines after opening braces and before closing braces will be removed.
282+
283+
`NoEmptyLinesOpeningClosingBraces` rule can format your code automatically.
284+
274285
### NoEmptyTrailingClosureParentheses
275286

276287
Function calls with no arguments and a trailing closure should not have empty parentheses.

Sources/SwiftFormat/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ add_library(SwiftFormat
7373
Rules/NoAssignmentInExpressions.swift
7474
Rules/NoBlockComments.swift
7575
Rules/NoCasesWithOnlyFallthrough.swift
76+
Rules/NoEmptyLineOpeningClosingBraces.swift
7677
Rules/NoEmptyTrailingClosureParentheses.swift
7778
Rules/NoLabelsInCasePatterns.swift
7879
Rules/NoLeadingUnderscores.swift

Sources/SwiftFormat/Core/Pipelines+Generated.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ class LintPipeline: SyntaxVisitor {
3636
super.init(viewMode: .sourceAccurate)
3737
}
3838

39+
override func visit(_ node: AccessorBlockSyntax) -> SyntaxVisitorContinueKind {
40+
visitIfEnabled(NoEmptyLinesOpeningClosingBraces.visit, for: node)
41+
return .visitChildren
42+
}
43+
override func visitPost(_ node: AccessorBlockSyntax) {
44+
onVisitPost(rule: NoEmptyLinesOpeningClosingBraces.self, for: node)
45+
}
46+
3947
override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
4048
visitIfEnabled(AllPublicDeclarationsHaveDocumentation.visit, for: node)
4149
visitIfEnabled(TypeNamesShouldBeCapitalized.visit, for: node)
@@ -93,10 +101,12 @@ class LintPipeline: SyntaxVisitor {
93101
}
94102

95103
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
104+
visitIfEnabled(NoEmptyLinesOpeningClosingBraces.visit, for: node)
96105
visitIfEnabled(OmitExplicitReturns.visit, for: node)
97106
return .visitChildren
98107
}
99108
override func visitPost(_ node: ClosureExprSyntax) {
109+
onVisitPost(rule: NoEmptyLinesOpeningClosingBraces.self, for: node)
100110
onVisitPost(rule: OmitExplicitReturns.self, for: node)
101111
}
102112

@@ -134,10 +144,12 @@ class LintPipeline: SyntaxVisitor {
134144

135145
override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind {
136146
visitIfEnabled(AmbiguousTrailingClosureOverload.visit, for: node)
147+
visitIfEnabled(NoEmptyLinesOpeningClosingBraces.visit, for: node)
137148
return .visitChildren
138149
}
139150
override func visitPost(_ node: CodeBlockSyntax) {
140151
onVisitPost(rule: AmbiguousTrailingClosureOverload.self, for: node)
152+
onVisitPost(rule: NoEmptyLinesOpeningClosingBraces.self, for: node)
141153
}
142154

143155
override func visit(_ node: ConditionElementSyntax) -> SyntaxVisitorContinueKind {
@@ -384,10 +396,12 @@ class LintPipeline: SyntaxVisitor {
384396

385397
override func visit(_ node: MemberBlockSyntax) -> SyntaxVisitorContinueKind {
386398
visitIfEnabled(AmbiguousTrailingClosureOverload.visit, for: node)
399+
visitIfEnabled(NoEmptyLinesOpeningClosingBraces.visit, for: node)
387400
return .visitChildren
388401
}
389402
override func visitPost(_ node: MemberBlockSyntax) {
390403
onVisitPost(rule: AmbiguousTrailingClosureOverload.self, for: node)
404+
onVisitPost(rule: NoEmptyLinesOpeningClosingBraces.self, for: node)
391405
}
392406

393407
override func visit(_ node: OptionalBindingConditionSyntax) -> SyntaxVisitorContinueKind {
@@ -411,10 +425,12 @@ class LintPipeline: SyntaxVisitor {
411425
}
412426

413427
override func visit(_ node: PrecedenceGroupDeclSyntax) -> SyntaxVisitorContinueKind {
428+
visitIfEnabled(NoEmptyLinesOpeningClosingBraces.visit, for: node)
414429
visitIfEnabled(NoLeadingUnderscores.visit, for: node)
415430
return .visitChildren
416431
}
417432
override func visitPost(_ node: PrecedenceGroupDeclSyntax) {
433+
onVisitPost(rule: NoEmptyLinesOpeningClosingBraces.self, for: node)
418434
onVisitPost(rule: NoLeadingUnderscores.self, for: node)
419435
}
420436

@@ -511,10 +527,12 @@ class LintPipeline: SyntaxVisitor {
511527
}
512528

513529
override func visit(_ node: SwitchExprSyntax) -> SyntaxVisitorContinueKind {
530+
visitIfEnabled(NoEmptyLinesOpeningClosingBraces.visit, for: node)
514531
visitIfEnabled(NoParensAroundConditions.visit, for: node)
515532
return .visitChildren
516533
}
517534
override func visitPost(_ node: SwitchExprSyntax) {
535+
onVisitPost(rule: NoEmptyLinesOpeningClosingBraces.self, for: node)
518536
onVisitPost(rule: NoParensAroundConditions.self, for: node)
519537
}
520538

@@ -597,6 +615,7 @@ extension FormatPipeline {
597615
node = NoAccessLevelOnExtensionDeclaration(context: context).rewrite(node)
598616
node = NoAssignmentInExpressions(context: context).rewrite(node)
599617
node = NoCasesWithOnlyFallthrough(context: context).rewrite(node)
618+
node = NoEmptyLinesOpeningClosingBraces(context: context).rewrite(node)
600619
node = NoEmptyTrailingClosureParentheses(context: context).rewrite(node)
601620
node = NoLabelsInCasePatterns(context: context).rewrite(node)
602621
node = NoParensAroundConditions(context: context).rewrite(node)

Sources/SwiftFormat/Core/RuleNameCache+Generated.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public let ruleNameCache: [ObjectIdentifier: String] = [
3434
ObjectIdentifier(NoAssignmentInExpressions.self): "NoAssignmentInExpressions",
3535
ObjectIdentifier(NoBlockComments.self): "NoBlockComments",
3636
ObjectIdentifier(NoCasesWithOnlyFallthrough.self): "NoCasesWithOnlyFallthrough",
37+
ObjectIdentifier(NoEmptyLinesOpeningClosingBraces.self): "NoEmptyLinesOpeningClosingBraces",
3738
ObjectIdentifier(NoEmptyTrailingClosureParentheses.self): "NoEmptyTrailingClosureParentheses",
3839
ObjectIdentifier(NoLabelsInCasePatterns.self): "NoLabelsInCasePatterns",
3940
ObjectIdentifier(NoLeadingUnderscores.self): "NoLeadingUnderscores",

Sources/SwiftFormat/Core/RuleRegistry+Generated.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"NoAssignmentInExpressions": true,
3434
"NoBlockComments": true,
3535
"NoCasesWithOnlyFallthrough": true,
36+
"NoEmptyLinesOpeningClosingBraces": false,
3637
"NoEmptyTrailingClosureParentheses": true,
3738
"NoLabelsInCasePatterns": true,
3839
"NoLeadingUnderscores": false,
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
15+
/// Empty lines are forbidden after opening braces and before closing braces.
16+
///
17+
/// Lint: Empty lines after opening braces and before closing braces yield a lint error.
18+
///
19+
/// Format: Empty lines after opening braces and before closing braces will be removed.
20+
@_spi(Rules)
21+
public final class NoEmptyLinesOpeningClosingBraces: SyntaxFormatRule {
22+
public override class var isOptIn: Bool { return true }
23+
24+
public override func visit(_ node: AccessorBlockSyntax) -> AccessorBlockSyntax {
25+
var result = node
26+
switch node.accessors {
27+
case .accessors(let accessors):
28+
result.accessors = .init(rewritten(accessors))
29+
case .getter(let getter):
30+
result.accessors = .init(rewritten(getter))
31+
}
32+
result.rightBrace = rewritten(node.rightBrace)
33+
return result
34+
}
35+
36+
public override func visit(_ node: CodeBlockSyntax) -> CodeBlockSyntax {
37+
var result = node
38+
result.statements = rewritten(node.statements)
39+
result.rightBrace = rewritten(node.rightBrace)
40+
return result
41+
}
42+
43+
public override func visit(_ node: MemberBlockSyntax) -> MemberBlockSyntax {
44+
var result = node
45+
result.members = rewritten(node.members)
46+
result.rightBrace = rewritten(node.rightBrace)
47+
return result
48+
}
49+
50+
public override func visit(_ node: ClosureExprSyntax) -> ExprSyntax {
51+
var result = node
52+
result.statements = rewritten(node.statements)
53+
result.rightBrace = rewritten(node.rightBrace)
54+
return ExprSyntax(result)
55+
}
56+
57+
public override func visit(_ node: SwitchExprSyntax) -> ExprSyntax {
58+
var result = node
59+
result.cases = rewritten(node.cases)
60+
result.rightBrace = rewritten(node.rightBrace)
61+
return ExprSyntax(result)
62+
}
63+
64+
public override func visit(_ node: PrecedenceGroupDeclSyntax) -> DeclSyntax {
65+
var result = node
66+
result.attributes = rewritten(node.attributes)
67+
result.rightBrace = rewritten(node.rightBrace)
68+
return DeclSyntax(result)
69+
}
70+
71+
func rewritten(_ token: TokenSyntax) -> TokenSyntax {
72+
let (trimmedLeadingTrivia, count) = token.leadingTrivia.trimmingSuperfluousNewlines()
73+
if trimmedLeadingTrivia.sourceLength != token.leadingTriviaLength {
74+
diagnose(.removeEmptyLinesBefore(count), on: token, anchor: .start)
75+
return token.with(\.leadingTrivia, trimmedLeadingTrivia)
76+
} else {
77+
return token
78+
}
79+
}
80+
81+
func rewritten<C: SyntaxCollection>(_ collection: C) -> C {
82+
var result = collection
83+
if let first = collection.first, first.leadingTrivia.containsNewlines,
84+
let index = collection.index(of: first)
85+
{
86+
let (trimmedLeadingTrivia, count) = first.leadingTrivia.trimmingSuperfluousNewlines()
87+
if trimmedLeadingTrivia.sourceLength != first.leadingTriviaLength {
88+
diagnose(.removeEmptyLinesAfter(count), on: first, anchor: .leadingTrivia(0))
89+
result[index] = first.with(\.leadingTrivia, trimmedLeadingTrivia)
90+
}
91+
}
92+
return rewrite(result).as(C.self)!
93+
}
94+
}
95+
96+
extension Trivia {
97+
func trimmingSuperfluousNewlines() -> (Trivia, Int) {
98+
var trimmmed = 0
99+
let pieces = self.indices.reduce([TriviaPiece]()) { (partialResult, index) in
100+
let piece = self[index]
101+
// Collapse consecutive newlines into a single one
102+
if case .newlines(let count) = piece {
103+
if let last = partialResult.last, last.isNewline {
104+
trimmmed += count
105+
return partialResult
106+
} else {
107+
trimmmed += count - 1
108+
return partialResult + [.newlines(1)]
109+
}
110+
}
111+
// Remove spaces/tabs surrounded by newlines
112+
if piece.isSpaceOrTab, index > 0, index < self.count - 1, self[index - 1].isNewline, self[index + 1].isNewline {
113+
return partialResult
114+
}
115+
// Retain other trivia pieces
116+
return partialResult + [piece]
117+
}
118+
119+
return (Trivia(pieces: pieces), trimmmed)
120+
}
121+
}
122+
123+
extension Finding.Message {
124+
fileprivate static func removeEmptyLinesAfter(_ count: Int) -> Finding.Message {
125+
"remove empty \(count > 1 ? "lines" : "line") after '{'"
126+
}
127+
128+
fileprivate static func removeEmptyLinesBefore(_ count: Int) -> Finding.Message {
129+
"remove empty \(count > 1 ? "lines" : "line") before '}'"
130+
}
131+
}

0 commit comments

Comments
 (0)