Skip to content

Commit 736aed3

Browse files
Add NoEmptyLinesOpeningClosingBraces rule
1 parent 021a5ab commit 736aed3

File tree

6 files changed

+298
-0
lines changed

6 files changed

+298
-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/Core/Pipelines+Generated.swift

Lines changed: 15 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 {
@@ -597,6 +611,7 @@ extension FormatPipeline {
597611
node = NoAccessLevelOnExtensionDeclaration(context: context).rewrite(node)
598612
node = NoAssignmentInExpressions(context: context).rewrite(node)
599613
node = NoCasesWithOnlyFallthrough(context: context).rewrite(node)
614+
node = NoEmptyLinesOpeningClosingBraces(context: context).rewrite(node)
600615
node = NoEmptyTrailingClosureParentheses(context: context).rewrite(node)
601616
node = NoLabelsInCasePatterns(context: context).rewrite(node)
602617
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": true,
3637
"NoEmptyTrailingClosureParentheses": true,
3738
"NoLabelsInCasePatterns": true,
3839
"NoLeadingUnderscores": false,
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2019 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+
@_spi(Rules)
16+
public final class NoEmptyLinesOpeningClosingBraces: SyntaxFormatRule {
17+
public override func visit(_ node: AccessorBlockSyntax) -> AccessorBlockSyntax {
18+
var result = node
19+
switch node.accessors {
20+
case .accessors(let accessors):
21+
result.accessors = .init(rewritten(accessors))
22+
case .getter(let getter):
23+
result.accessors = .init(rewritten(getter))
24+
}
25+
result.rightBrace = rewritten(node.rightBrace)
26+
return result
27+
}
28+
29+
public override func visit(_ node: CodeBlockSyntax) -> CodeBlockSyntax {
30+
var result = node
31+
result.statements = rewritten(node.statements)
32+
result.rightBrace = rewritten(node.rightBrace)
33+
return result
34+
}
35+
36+
public override func visit(_ node: MemberBlockSyntax) -> MemberBlockSyntax {
37+
var result = node
38+
result.members = rewritten(node.members)
39+
result.rightBrace = rewritten(node.rightBrace)
40+
return result
41+
}
42+
43+
public override func visit(_ node: ClosureExprSyntax) -> ExprSyntax {
44+
var result = node
45+
result.statements = rewritten(node.statements)
46+
result.rightBrace = rewritten(node.rightBrace)
47+
return ExprSyntax(result)
48+
}
49+
50+
func rewritten(_ token: TokenSyntax) -> TokenSyntax {
51+
let (trimmedLeadingTrivia, count) = token.leadingTrivia._trimmingSuperfluousNewlines()
52+
if trimmedLeadingTrivia.sourceLength != token.leadingTriviaLength {
53+
diagnose(.removeEmptyLinesBefore(count), on: token, anchor: .start)
54+
return token.with(\.leadingTrivia, trimmedLeadingTrivia)
55+
} else {
56+
return token
57+
}
58+
}
59+
60+
func rewritten<C: SyntaxCollection>(_ collection: C) -> C {
61+
var result = collection
62+
if let first = collection.first, first.leadingTrivia.containsNewlines,
63+
let index = collection.index(of: first)
64+
{
65+
let (trimmedLeadingTrivia, count) = first.leadingTrivia._trimmingSuperfluousNewlines()
66+
if trimmedLeadingTrivia.sourceLength != first.leadingTriviaLength {
67+
diagnose(.removeEmptyLinesAfter(count), on: first, anchor: .leadingTrivia(0))
68+
result[index] = first.with(\.leadingTrivia, trimmedLeadingTrivia)
69+
}
70+
}
71+
return rewrite(result).as(C.self)!
72+
}
73+
}
74+
75+
extension Trivia {
76+
func trimmingSuperfluousNewlines() -> Trivia {
77+
let pieces = self.indices.reduce([TriviaPiece]()) { (partialResult, index) in
78+
let piece = self[index]
79+
// Collapse consecutive newlines into a single one
80+
if piece.isNewline {
81+
if let last = partialResult.last, last.isNewline {
82+
return partialResult
83+
} else {
84+
return partialResult + [.newlines(1)]
85+
}
86+
}
87+
// Remove spaces/tabs surrounded by newlines
88+
if piece.isSpaceOrTab, index > 0, index < self.count - 1, self[index - 1].isNewline, self[index + 1].isNewline {
89+
return partialResult
90+
}
91+
// Retain other trivia pieces
92+
return partialResult + [piece]
93+
}
94+
return Trivia(pieces: pieces)
95+
}
96+
func _trimmingSuperfluousNewlines() -> (Trivia, Int) {
97+
var trimmmed = 0
98+
let pieces = self.indices.reduce([TriviaPiece]()) { (partialResult, index) in
99+
let piece = self[index]
100+
// Collapse consecutive newlines into a single one
101+
if case .newlines(let count) = piece {
102+
if let last = partialResult.last, last.isNewline {
103+
trimmmed += count
104+
return partialResult
105+
} else {
106+
trimmmed += count - 1
107+
return partialResult + [.newlines(1)]
108+
}
109+
}
110+
// Remove spaces/tabs surrounded by newlines
111+
if piece.isSpaceOrTab, index > 0, index < self.count - 1, self[index - 1].isNewline, self[index + 1].isNewline {
112+
return partialResult
113+
}
114+
// Retain other trivia pieces
115+
return partialResult + [piece]
116+
}
117+
118+
return (Trivia(pieces: pieces), trimmmed)
119+
}
120+
}
121+
122+
extension Finding.Message {
123+
fileprivate static func removeEmptyLinesAfter(_ count: Int) -> Finding.Message {
124+
"remove empty \(count > 1 ? "lines" : "line") after '{'"
125+
}
126+
127+
fileprivate static func removeEmptyLinesBefore(_ count: Int) -> Finding.Message {
128+
"remove empty \(count > 1 ? "lines" : "line") before '}'"
129+
}
130+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import _SwiftFormatTestSupport
2+
3+
@_spi(Rules) import SwiftFormat
4+
5+
final class NoEmptyLinesOpeningClosingBracesTests: LintOrFormatRuleTestCase {
6+
func testNoEmptyLinesOpeningClosingBracesInCodeBlock() {
7+
assertFormatting(
8+
NoEmptyLinesOpeningClosingBraces.self,
9+
input: """
10+
func f() {1️⃣
11+
12+
//
13+
return
14+
15+
16+
2️⃣}
17+
""",
18+
expected: """
19+
func f() {
20+
//
21+
return
22+
}
23+
""",
24+
findings: [
25+
FindingSpec("1️⃣", message: "remove empty line after '{'"),
26+
FindingSpec("2️⃣", message: "remove empty lines before '}'"),
27+
]
28+
)
29+
}
30+
31+
func testNoEmptyLinesOpeningClosingBracesInMemberBlock() {
32+
assertFormatting(
33+
NoEmptyLinesOpeningClosingBraces.self,
34+
input: """
35+
struct {1️⃣
36+
37+
let x: Int
38+
39+
let y: Int
40+
41+
2️⃣}
42+
""",
43+
expected: """
44+
struct {
45+
let x: Int
46+
47+
let y: Int
48+
}
49+
""",
50+
findings: [
51+
FindingSpec("1️⃣", message: "remove empty line after '{'"),
52+
FindingSpec("2️⃣", message: "remove empty line before '}'"),
53+
]
54+
)
55+
}
56+
57+
func testNoEmptyLinesOpeningClosingBracesInAccessorBlock() {
58+
assertFormatting(
59+
NoEmptyLinesOpeningClosingBraces.self,
60+
input: """
61+
var x: Int {1️⃣
62+
63+
//
64+
return _x
65+
66+
2️⃣}
67+
68+
var y: Int {3️⃣
69+
70+
get 5️⃣{
71+
72+
//
73+
return _y
74+
75+
6️⃣ }
76+
77+
set 7️⃣{
78+
79+
//
80+
_x = newValue
81+
82+
8️⃣ }
83+
84+
4️⃣}
85+
""",
86+
expected: """
87+
var x: Int {
88+
//
89+
return _x
90+
}
91+
92+
var y: Int {
93+
get {
94+
//
95+
return _y
96+
}
97+
98+
set {
99+
//
100+
_x = newValue
101+
}
102+
}
103+
""",
104+
findings: [
105+
FindingSpec("1️⃣", message: "remove empty line after '{'"),
106+
FindingSpec("2️⃣", message: "remove empty line before '}'"),
107+
FindingSpec("3️⃣", message: "remove empty line after '{'"),
108+
FindingSpec("4️⃣", message: "remove empty line before '}'"),
109+
FindingSpec("5️⃣", message: "remove empty line after '{'"),
110+
FindingSpec("6️⃣", message: "remove empty line before '}'"),
111+
FindingSpec("7️⃣", message: "remove empty line after '{'"),
112+
FindingSpec("8️⃣", message: "remove empty line before '}'"),
113+
]
114+
)
115+
}
116+
117+
func testNoEmptyLinesOpeningClosingBracesInClosureExpr() {
118+
assertFormatting(
119+
NoEmptyLinesOpeningClosingBraces.self,
120+
input: """
121+
let closure = {1️⃣
122+
123+
//
124+
return
125+
126+
2️⃣}
127+
""",
128+
expected: """
129+
let closure = {
130+
//
131+
return
132+
}
133+
""",
134+
findings: [
135+
FindingSpec("1️⃣", message: "remove empty line after '{'"),
136+
FindingSpec("2️⃣", message: "remove empty line before '}'"),
137+
]
138+
)
139+
}
140+
}

0 commit comments

Comments
 (0)