Skip to content

Commit 4972d33

Browse files
atamez31allevato
authored andcommitted
Implementation of BlankLineBetweenMembers (swiftlang#76)
1 parent 9e66e68 commit 4972d33

File tree

2 files changed

+291
-0
lines changed

2 files changed

+291
-0
lines changed

tools/swift-format/Sources/Rules/BlankLineBetweenMembers.swift

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,147 @@ import SwiftSyntax
1717
///
1818
/// - SeeAlso: https://google.github.io/swift#vertical-whitespace
1919
public final class BlankLineBetweenMembers: SyntaxFormatRule {
20+
public override func visit(_ node: MemberDeclBlockSyntax) -> Syntax {
21+
var membersList = [MemberDeclListItemSyntax]()
22+
var hasValidNumOfBlankLines = true
2023

24+
// Iterates through all the declaration of the member, to ensure that the declarations have
25+
// at least on blank line and doesn't exceed the maximum number of blank lines.
26+
for member in node.members {
27+
let currentMember = checkForNestedMembers(member)
28+
guard let memberTrivia = currentMember.leadingTrivia else { continue }
29+
let triviaWithoutTrailingSpaces = memberTrivia.withoutTrailingSpaces()
30+
guard let firstPiece = triviaWithoutTrailingSpaces.first else { continue }
31+
32+
if exceedsMaxBlankLines(triviaWithoutTrailingSpaces) {
33+
let correctTrivia = removeExtraBlankLines(triviaWithoutTrailingSpaces, currentMember)
34+
let newMember = replaceTrivia(
35+
on: currentMember,
36+
token: currentMember.firstToken!,
37+
leadingTrivia: correctTrivia
38+
) as! MemberDeclListItemSyntax
39+
40+
hasValidNumOfBlankLines = false
41+
membersList.append(newMember)
42+
}
43+
// Ensures that there is at least one blank line between each member of a type.
44+
// Unless is a single-line declaration and the format is configured to
45+
// ignored them.
46+
else if case .newlines(let numNewLines) = firstPiece,
47+
!ignoreItem(item: currentMember),
48+
numNewLines == 1 {
49+
let numBlankLines = member != node.members.first ? 1 : 0
50+
let correctTrivia = Trivia.newlines(numBlankLines) + memberTrivia
51+
let newMember = replaceTrivia(
52+
on: currentMember, token: currentMember.firstToken!,
53+
leadingTrivia: correctTrivia
54+
) as! MemberDeclListItemSyntax
55+
56+
diagnose(.addBlankLine, on: currentMember)
57+
hasValidNumOfBlankLines = false
58+
membersList.append(newMember)
59+
}
60+
else {
61+
membersList.append(member)
62+
}
63+
}
64+
65+
return hasValidNumOfBlankLines ? node :
66+
node.withMembers(SyntaxFactory.makeMemberDeclList(membersList))
67+
}
68+
69+
/// Indicates if the given trivia has more than
70+
/// the maximum number of blank lines.
71+
func exceedsMaxBlankLines(_ trivia: Trivia) -> Bool {
72+
let maxBlankLines = context.configuration.maximumBlankLines
73+
74+
for piece in trivia {
75+
if case .newlines(let num) = piece,
76+
num - 1 > maxBlankLines {
77+
return true
78+
}
79+
}
80+
return false
81+
}
82+
83+
/// Returns the given trivia without any set of consecutive blank lines
84+
/// that exceeds the maximumBlankLines.
85+
func removeExtraBlankLines(_ trivia: Trivia, _ member: MemberDeclListItemSyntax) -> Trivia {
86+
let maxBlankLines = context.configuration.maximumBlankLines
87+
var pieces = [TriviaPiece]()
88+
89+
// Iterates through the trivia, verifying that the number of blank
90+
// lines in the file do not exceed the maximumBlankLines. If it does
91+
// a lint error is raised.
92+
for piece in trivia {
93+
if case .newlines(let num) = piece,
94+
num - 1 > maxBlankLines {
95+
pieces.append(.newlines(maxBlankLines + 1))
96+
diagnose(.removeBlankLines(count: num - maxBlankLines), on: member)
97+
}
98+
else {
99+
pieces.append(piece)
100+
}
101+
}
102+
return Trivia(pieces: pieces)
103+
}
104+
105+
/// Indicates if a declaration has to be ignored by checking if it's
106+
/// a single line and if the format is configured to ignore single lines.
107+
func ignoreItem(item: MemberDeclListItemSyntax) -> Bool {
108+
guard let firstToken = item.firstToken else { return false }
109+
guard let lastToken = item.lastToken else { return false }
110+
111+
let isSingleLine = firstToken.positionAfterSkippingLeadingTrivia.line ==
112+
lastToken.positionAfterSkippingLeadingTrivia.line
113+
114+
let ignoreLine = context.configuration.blankLineBetweenMembers
115+
.ignoreSingleLineProperties
116+
117+
return isSingleLine && ignoreLine
118+
}
119+
120+
/// Recursively ensures all nested member types follows the BlankLineBetweenMembers rule.
121+
func checkForNestedMembers(_ member: MemberDeclListItemSyntax) -> MemberDeclListItemSyntax {
122+
switch member.decl {
123+
case let nestedEnum as EnumDeclSyntax:
124+
let nestedMembers = visit(nestedEnum.members)
125+
let newDecl = nestedEnum.withMembers(nestedMembers as? MemberDeclBlockSyntax)
126+
return member.withDecl(newDecl)
127+
case let nestedStruct as StructDeclSyntax:
128+
let nestedMembers = visit(nestedStruct.members)
129+
let newDecl = nestedStruct.withMembers(nestedMembers as? MemberDeclBlockSyntax)
130+
return member.withDecl(newDecl)
131+
case let nestedClass as ClassDeclSyntax:
132+
let nestedMembers = visit(nestedClass.members)
133+
let newDecl = nestedClass.withMembers(nestedMembers as? MemberDeclBlockSyntax)
134+
return member.withDecl(newDecl)
135+
case let nestedExtension as ExtensionDeclSyntax:
136+
let nestedMembers = visit(nestedExtension.members)
137+
let newDecl = nestedExtension.withMembers(nestedMembers as? MemberDeclBlockSyntax)
138+
return member.withDecl(newDecl)
139+
default:
140+
return member
141+
}
142+
}
143+
}
144+
145+
/// Indicates if the given trivia piece is any type of comment.
146+
func isComment(_ triviaPiece: TriviaPiece) -> Bool {
147+
switch triviaPiece {
148+
case .lineComment(_), .docLineComment(_),
149+
.blockComment(_), .docBlockComment(_):
150+
return true
151+
default:
152+
return false
153+
}
154+
}
155+
156+
extension Diagnostic.Message {
157+
static let addBlankLine = Diagnostic.Message(.warning, "add one blank line between declarations")
158+
159+
static func removeBlankLines(count: Int) -> Diagnostic.Message {
160+
let ending = count > 1 ? "s" : ""
161+
return Diagnostic.Message(.warning, "remove \(count) blank line\(ending)")
162+
}
21163
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import Foundation
2+
import XCTest
3+
import SwiftSyntax
4+
5+
@testable import Rules
6+
7+
public class BlankLineBetweenMembersTests: DiagnosingTestCase {
8+
public func testInvalidBlankLineBetweenMembers() {
9+
XCTAssertFormatting(
10+
BlankLineBetweenMembers.self,
11+
input: """
12+
struct foo1 {
13+
14+
15+
16+
var test1 = 13
17+
// Multiline
18+
// comment for b
19+
var b = 12
20+
/*BlockComment*/
21+
22+
23+
var c = 11
24+
25+
26+
27+
28+
// Multiline comment
29+
// for d
30+
var d: Bool {
31+
return false
32+
}
33+
/// Comment for e
34+
var end1: Bool {
35+
return false
36+
}
37+
}
38+
""",
39+
expected: """
40+
struct foo1 {
41+
42+
var test1 = 13
43+
// Multiline
44+
// comment for b
45+
var b = 12
46+
/*BlockComment*/
47+
48+
var c = 11
49+
50+
// Multiline comment
51+
// for d
52+
var d: Bool {
53+
return false
54+
}
55+
56+
/// Comment for e
57+
var end1: Bool {
58+
return false
59+
}
60+
}
61+
""")
62+
}
63+
64+
public func testTwoMembers() {
65+
XCTAssertFormatting(
66+
BlankLineBetweenMembers.self,
67+
input: """
68+
struct foo2 {
69+
var test2 = 13
70+
71+
var a = 10
72+
}
73+
74+
struct secondFoo2 {
75+
var a = 1
76+
var end2: Bool {
77+
return false
78+
}
79+
}
80+
""",
81+
expected: """
82+
struct foo2 {
83+
var test2 = 13
84+
85+
var a = 10
86+
}
87+
88+
struct secondFoo2 {
89+
var a = 1
90+
91+
var end2: Bool {
92+
return false
93+
}
94+
}
95+
""")
96+
}
97+
98+
public func testNestedMembers() {
99+
XCTAssertFormatting(
100+
BlankLineBetweenMembers.self,
101+
input: """
102+
struct foo3 {
103+
// nested Rank enumeration
104+
enum Rank: Int {
105+
case two = 2, three, four
106+
107+
108+
case jack, queen, king, ace
109+
}
110+
111+
112+
113+
struct secondFoo3 {
114+
var a = 1
115+
var e: Bool {
116+
return false
117+
}
118+
}
119+
}
120+
""",
121+
expected: """
122+
struct foo3 {
123+
// nested Rank enumeration
124+
enum Rank: Int {
125+
case two = 2, three, four
126+
127+
case jack, queen, king, ace
128+
}
129+
130+
struct secondFoo3 {
131+
var a = 1
132+
133+
var e: Bool {
134+
return false
135+
}
136+
}
137+
}
138+
""")
139+
}
140+
141+
#if !os(macOS)
142+
static let allTests = [
143+
BlankLineBetweenMembersTests.testInvalidBlankLineBetweenMembers,
144+
BlankLineBetweenMembersTests.testTwoMembers,
145+
BlankLineBetweenMembersTests.testNestedMembers,
146+
147+
]
148+
#endif
149+
}

0 commit comments

Comments
 (0)