Skip to content

Commit 03b1ba3

Browse files
atamez31allevato
authored andcommitted
Implement maximum blank lines (swiftlang#80)
1 parent 8042903 commit 03b1ba3

File tree

3 files changed

+227
-0
lines changed

3 files changed

+227
-0
lines changed

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

+67
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,72 @@ import SwiftSyntax
1616
///
1717
/// - SeeAlso: https://google.github.io/swift#vertical-whitespace
1818
public final class MaximumBlankLines: SyntaxFormatRule {
19+
public override func visit(_ token: TokenSyntax) -> Syntax {
20+
guard let parentToken = token.parent else {
21+
return token.withLeadingTrivia(removeExtraBlankLines(token).newTrivia)
22+
}
1923

24+
guard let grandparentTok = parentToken.parent else {
25+
return token.withLeadingTrivia(removeExtraBlankLines(token).newTrivia)
26+
}
27+
28+
// Tokens who appeared in a member type are handle by
29+
// BlankLineBetweenMembers rule.
30+
if grandparentTok is MemberDeclListItemSyntax {
31+
return token
32+
}
33+
else {
34+
let (correctTrivia, hasValidAmountOfBlankLines) = removeExtraBlankLines(token)
35+
return hasValidAmountOfBlankLines ? token : token.withLeadingTrivia(correctTrivia)
36+
}
37+
}
38+
39+
/// Indicates if the given trivia contains an invalid amount of
40+
/// consecutively blank lines. If it does it returns a clean trivia
41+
/// with the correct amount of blank lines.
42+
func removeExtraBlankLines(_ token: TokenSyntax) -> (
43+
newTrivia: Trivia,
44+
hasValidAmountOfBlankLines: Bool
45+
) {
46+
let maxBlankLines = context.configuration.maximumBlankLines
47+
var pieces = [TriviaPiece]()
48+
let isTheFirstOne = token == token.root.firstToken
49+
let tokenTrivia = token.leadingTrivia
50+
var startsIn = 0
51+
var hasValidAmountOfBlankLines = true
52+
let triviaWithoutTrailingSpaces = tokenTrivia.withoutTrailingSpaces()
53+
54+
// Ensures the beginning of file doesn't have an invalid amount of blank line.
55+
// The first triviapiece of a file is a special case, where each newline is
56+
// a blank line.
57+
if isTheFirstOne, let firstPiece = triviaWithoutTrailingSpaces.first,
58+
case .newlines(let num) = firstPiece, num > maxBlankLines {
59+
pieces.append(.newlines(maxBlankLines))
60+
diagnose(.removeMaxBlankLines(count: num - maxBlankLines), on: token)
61+
startsIn = 1
62+
hasValidAmountOfBlankLines = false
63+
}
64+
65+
// Iterates through the token trivia, verifying that the number on blank
66+
// lines in the file do not exceed the maximumBlankLines.
67+
for index in startsIn..<triviaWithoutTrailingSpaces.count {
68+
if case .newlines(let num) = triviaWithoutTrailingSpaces[index],
69+
num - 1 > maxBlankLines {
70+
pieces.append(.newlines(maxBlankLines + 1))
71+
diagnose(.removeMaxBlankLines(count: num - maxBlankLines), on: token)
72+
hasValidAmountOfBlankLines = false
73+
}
74+
else {
75+
pieces.append(triviaWithoutTrailingSpaces[index])
76+
}
77+
}
78+
return (Trivia(pieces: pieces), hasValidAmountOfBlankLines)
79+
}
80+
}
81+
82+
extension Diagnostic.Message {
83+
static func removeMaxBlankLines(count: Int) -> Diagnostic.Message {
84+
let ending = count == 1 ? "" : "s"
85+
return Diagnostic.Message(.warning, "remove \(count) blank line\(ending)")
86+
}
2087
}

tools/swift-format/Sources/Rules/Trivia+Convenience.swift

+18
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,24 @@ extension Trivia {
2121
})
2222
}
2323

24+
/// Returns this set of trivia without any trailing whitespace characters.
25+
func withoutTrailingSpaces() -> Trivia {
26+
var pieces = [TriviaPiece]()
27+
guard var prev = first else { return self }
28+
for piece in dropFirst() {
29+
switch (prev, piece) {
30+
case (.spaces(_), .newlines(_)),
31+
(.tabs(_), .newlines(_)):
32+
prev = piece
33+
default:
34+
pieces.append(prev)
35+
prev = piece
36+
}
37+
}
38+
pieces.append(prev)
39+
return Trivia(pieces: pieces).condensed()
40+
}
41+
2442
/// Returns this set of trivia, without any newlines.
2543
func withoutNewlines() -> Trivia {
2644
return Trivia(pieces: filter {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import Foundation
2+
import XCTest
3+
import SwiftSyntax
4+
5+
@testable import Rules
6+
7+
public class MaximumBlankLinesTests: DiagnosingTestCase {
8+
public func testInvalidMaximumBlankLines() {
9+
XCTAssertFormatting(
10+
MaximumBlankLines.self,
11+
input: """
12+
13+
14+
/// Doc Comment
15+
16+
import Foundation
17+
let a = 1
18+
let b = 2
19+
20+
21+
22+
// Multiline
23+
24+
25+
// comment for b
26+
27+
28+
var b = 12
29+
30+
// eof
31+
32+
33+
""",
34+
expected: """
35+
36+
/// Doc Comment
37+
38+
import Foundation
39+
let a = 1
40+
let b = 2
41+
42+
// Multiline
43+
44+
// comment for b
45+
46+
var b = 12
47+
48+
// eof
49+
50+
51+
""")
52+
}
53+
54+
public func testInvalidWithMemberTypesMaximumBlankLines() {
55+
XCTAssertFormatting(
56+
MaximumBlankLines.self,
57+
input: """
58+
59+
struct foo {
60+
let a = 1
61+
62+
63+
let b = 2
64+
var isTest: Bool {
65+
66+
67+
68+
return true
69+
70+
71+
}
72+
73+
74+
75+
}
76+
77+
78+
79+
struct test {}
80+
81+
82+
""",
83+
expected: """
84+
85+
struct foo {
86+
let a = 1
87+
88+
89+
let b = 2
90+
var isTest: Bool {
91+
92+
return true
93+
94+
}
95+
96+
}
97+
98+
struct test {}
99+
100+
101+
""")
102+
}
103+
104+
public func testIgnoreMultilineStrings() {
105+
XCTAssertFormatting(
106+
MaximumBlankLines.self,
107+
input: """
108+
// Blanklines in multiline string
109+
// should be ignored.
110+
let a = 1
111+
112+
113+
let strMulti = \"""
114+
This is a multiline
115+
116+
117+
string
118+
\"""
119+
""",
120+
expected: """
121+
// Blanklines in multiline string
122+
// should be ignored.
123+
let a = 1
124+
125+
let strMulti = \"""
126+
This is a multiline
127+
128+
129+
string
130+
\"""
131+
""")
132+
}
133+
134+
#if !os(macOS)
135+
static let allTests = [
136+
MaximumBlankLinesTests.testInvalidBlankMaximumBlankLines,
137+
MaximumBlankLinesTests.testInvalidWithMemberTypesMaximumBlankLines,
138+
MaximumBlankLinesTests.testIgnoreMultilineStrings,
139+
140+
]
141+
#endif
142+
}

0 commit comments

Comments
 (0)