Skip to content

Commit b99c6e7

Browse files
authored
Merge pull request swiftlang#257 from google/format-blank-lines
Fix false diagnostics in BlankLinesBetweenMembers.
2 parents 3bb293b + b871dd0 commit b99c6e7

File tree

4 files changed

+148
-126
lines changed

4 files changed

+148
-126
lines changed

Sources/SwiftFormatCore/Syntax+Convenience.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,45 @@ extension Syntax {
2727
}
2828
return nil
2929
}
30+
31+
/// Returns true if the node occupies a single line.
32+
///
33+
/// - Parameters:
34+
/// - includingLeadingComment: If true, factor any possible leading comments into the
35+
/// determination of whether the node occupies a single line.
36+
/// - sourceLocationConverter: Used to convert source positions to line/column locations.
37+
/// - Returns: True if the node occupies a single line.
38+
public func isSingleLine(
39+
includingLeadingComment: Bool,
40+
sourceLocationConverter: SourceLocationConverter
41+
) -> Bool {
42+
guard let firstToken = firstToken, let lastToken = lastToken else { return true }
43+
44+
let startPosition: AbsolutePosition
45+
if includingLeadingComment {
46+
// Iterate over the trivia, stopping at the first comment, and using that as the start
47+
// position.
48+
var currentPosition = firstToken.position
49+
for piece in firstToken.leadingTrivia {
50+
switch piece {
51+
case .lineComment, .blockComment, .docLineComment, .docBlockComment:
52+
break
53+
default:
54+
currentPosition += piece.sourceLength
55+
}
56+
}
57+
startPosition = currentPosition
58+
}
59+
else {
60+
startPosition = firstToken.positionAfterSkippingLeadingTrivia
61+
}
62+
63+
let startLocation = sourceLocationConverter.location(for: startPosition)
64+
let endLocation = sourceLocationConverter.location(
65+
for: lastToken.endPositionBeforeTrailingTrivia)
66+
67+
return startLocation.line == endLocation.line
68+
}
3069
}
3170

3271
extension SyntaxCollection {

Sources/SwiftFormatCore/Trivia+Convenience.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import SwiftSyntax
1414

1515
extension Trivia {
16-
/// Returns the number of whitespace characters after this node.
16+
/// Returns the number of whitespace characters in this trivia.
1717
public var numberOfSpaces: Int {
1818
var count = 0
1919
for piece in self {
@@ -24,7 +24,7 @@ extension Trivia {
2424
return count
2525
}
2626

27-
/// Returns the number of newlines after this node.
27+
/// Returns the number of newlines in this trivia.
2828
public var numberOfNewlines: Int {
2929
var count = 0
3030
for piece in self {
@@ -35,6 +35,20 @@ extension Trivia {
3535
return count
3636
}
3737

38+
/// Returns the number of leading newlines in this trivia.
39+
public var numberOfLeadingNewlines: Int {
40+
var count = 0
41+
loop: for piece in self {
42+
switch piece {
43+
case .newlines(let n): count += n
44+
case .carriageReturns(let n): count += n
45+
case .carriageReturnLineFeeds(let n): count += n
46+
default: break loop
47+
}
48+
}
49+
return count
50+
}
51+
3852
public var numberOfComments: Int {
3953
var count = 0
4054
for piece in self {

Sources/SwiftFormatRules/BlankLineBetweenMembers.swift

Lines changed: 37 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -18,122 +18,62 @@ import SwiftSyntax
1818
///
1919
/// Optionally, declarations of single-line properties can be ignored.
2020
///
21-
/// Lint: If more than the maximum number of blank lines appear, a lint error is raised.
22-
/// If there are no blank lines between members, a lint error is raised.
21+
/// This rule does not check the maximum number of blank lines; the pretty printer clamps those
22+
/// as needed.
23+
///
24+
/// Lint: If there are no blank lines between members, a lint error is raised.
2325
///
2426
/// Format: Declarations with no blank lines will have a blank line inserted.
25-
/// Declarations with more than the maximum number of blank lines will be reduced to the
26-
/// maximum number of blank lines.
2727
///
28-
/// Configuration: maximumBlankLines, blankLineBetweenMembers.ignoreSingleLineProperties
28+
/// Configuration: blankLineBetweenMembers.ignoreSingleLineProperties
2929
///
3030
/// - SeeAlso: https://google.github.io/swift#vertical-whitespace
3131
public final class BlankLineBetweenMembers: SyntaxFormatRule {
3232
public override func visit(_ node: MemberDeclBlockSyntax) -> Syntax {
33-
var membersList = [MemberDeclListItemSyntax]()
34-
var hasValidNumOfBlankLines = true
33+
guard let firstMember = node.members.first else { return super.visit(node) }
34+
35+
let ignoreSingleLine = context.configuration.blankLineBetweenMembers.ignoreSingleLineProperties
36+
37+
// The first member can just be added as-is; we don't force any newline before it.
38+
var membersList = [visitNestedDecls(of: firstMember)]
3539

3640
// Iterates through all the declaration of the member, to ensure that the declarations have
37-
// at least on blank line and doesn't exceed the maximum number of blank lines.
38-
for member in node.members {
39-
let currentMember = checkForNestedMembers(member)
40-
guard let memberTrivia = currentMember.leadingTrivia else { continue }
41-
let triviaWithoutTrailingSpaces = memberTrivia.withoutTrailingSpaces()
42-
guard let firstPiece = triviaWithoutTrailingSpaces.first else { continue }
41+
// at least one blank line between them when necessary.
42+
var previousMemberWasSingleLine = firstMember.isSingleLine(
43+
includingLeadingComment: true,
44+
sourceLocationConverter: context.sourceLocationConverter
45+
)
4346

44-
if exceedsMaxBlankLines(triviaWithoutTrailingSpaces) {
45-
let correctTrivia = removeExtraBlankLines(triviaWithoutTrailingSpaces, currentMember)
46-
let newMember = replaceTrivia(
47-
on: currentMember,
48-
token: currentMember.firstToken!,
49-
leadingTrivia: correctTrivia
50-
) as! MemberDeclListItemSyntax
51-
52-
hasValidNumOfBlankLines = false
53-
membersList.append(newMember)
54-
}
55-
// Ensures that there is at least one blank line between each member of a type.
56-
// Unless is a single-line declaration and the format is configured to
57-
// ignored them.
58-
else if case .newlines(let numNewLines) = firstPiece,
59-
!ignoreItem(item: currentMember),
60-
numNewLines == 1 {
61-
let numBlankLines = member.indexInParent == 0 ? 0 : 1
62-
let correctTrivia = Trivia.newlines(numBlankLines) + memberTrivia
63-
let newMember = replaceTrivia(
64-
on: currentMember, token: currentMember.firstToken!,
47+
for member in node.members.dropFirst() {
48+
var memberToAdd = visitNestedDecls(of: member)
49+
50+
let memberIsSingleLine = memberToAdd.isSingleLine(
51+
includingLeadingComment: true,
52+
sourceLocationConverter: context.sourceLocationConverter
53+
)
54+
55+
if let memberTrivia = memberToAdd.leadingTrivia,
56+
!(ignoreSingleLine && previousMemberWasSingleLine && memberIsSingleLine)
57+
&& memberTrivia.numberOfLeadingNewlines == 1 // Only one newline => no blank line
58+
{
59+
let correctTrivia = Trivia.newlines(1) + memberTrivia
60+
memberToAdd = replaceTrivia(
61+
on: memberToAdd, token: memberToAdd.firstToken!,
6562
leadingTrivia: correctTrivia
6663
) as! MemberDeclListItemSyntax
67-
68-
diagnose(.addBlankLine, on: currentMember)
69-
hasValidNumOfBlankLines = false
70-
membersList.append(newMember)
71-
}
72-
else {
73-
membersList.append(member)
74-
}
75-
}
76-
77-
return hasValidNumOfBlankLines ? node :
78-
node.withMembers(SyntaxFactory.makeMemberDeclList(membersList))
79-
}
80-
81-
/// Indicates if the given trivia has more than
82-
/// the maximum number of blank lines.
83-
func exceedsMaxBlankLines(_ trivia: Trivia) -> Bool {
84-
let maxBlankLines = context.configuration.maximumBlankLines
8564

86-
for piece in trivia {
87-
if case .newlines(let num) = piece,
88-
num - 1 > maxBlankLines {
89-
return true
65+
diagnose(.addBlankLine, on: memberToAdd)
9066
}
91-
}
92-
return false
93-
}
94-
95-
/// Returns the given trivia without any set of consecutive blank lines
96-
/// that exceeds the maximumBlankLines.
97-
func removeExtraBlankLines(_ trivia: Trivia, _ member: MemberDeclListItemSyntax) -> Trivia {
98-
let maxBlankLines = context.configuration.maximumBlankLines
99-
var pieces = [TriviaPiece]()
100-
101-
// Iterates through the trivia, verifying that the number of blank
102-
// lines in the file do not exceed the maximumBlankLines. If it does
103-
// a lint error is raised.
104-
for piece in trivia {
105-
if case .newlines(let num) = piece,
106-
num - 1 > maxBlankLines {
107-
pieces.append(.newlines(maxBlankLines + 1))
108-
diagnose(.removeBlankLines(count: num - maxBlankLines), on: member)
109-
}
110-
else {
111-
pieces.append(piece)
112-
}
113-
}
114-
return Trivia(pieces: pieces)
115-
}
116-
117-
/// Indicates if a declaration has to be ignored by checking if it's
118-
/// a single line and if the format is configured to ignore single lines.
119-
func ignoreItem(item: MemberDeclListItemSyntax) -> Bool {
120-
guard let firstToken = item.firstToken else { return false }
121-
guard let lastToken = item.lastToken else { return false }
12267

123-
let firstTokenLocation = context.sourceLocationConverter.location(
124-
for: firstToken.positionAfterSkippingLeadingTrivia)
125-
let lastTokenLocation = context.sourceLocationConverter.location(
126-
for: lastToken.positionAfterSkippingLeadingTrivia)
127-
let isSingleLine = firstTokenLocation.line == lastTokenLocation.line
128-
129-
let ignoreLine = context.configuration.blankLineBetweenMembers
130-
.ignoreSingleLineProperties
68+
membersList.append(memberToAdd)
69+
previousMemberWasSingleLine = memberIsSingleLine
70+
}
13171

132-
return isSingleLine && ignoreLine
72+
return node.withMembers(SyntaxFactory.makeMemberDeclList(membersList))
13373
}
13474

13575
/// Recursively ensures all nested member types follows the BlankLineBetweenMembers rule.
136-
func checkForNestedMembers(_ member: MemberDeclListItemSyntax) -> MemberDeclListItemSyntax {
76+
func visitNestedDecls(of member: MemberDeclListItemSyntax) -> MemberDeclListItemSyntax {
13777
switch member.decl {
13878
case let nestedEnum as EnumDeclSyntax:
13979
let nestedMembers = visit(nestedEnum.members)
@@ -157,22 +97,6 @@ public final class BlankLineBetweenMembers: SyntaxFormatRule {
15797
}
15898
}
15999

160-
/// Indicates if the given trivia piece is any type of comment.
161-
func isComment(_ triviaPiece: TriviaPiece) -> Bool {
162-
switch triviaPiece {
163-
case .lineComment(_), .docLineComment(_),
164-
.blockComment(_), .docBlockComment(_):
165-
return true
166-
default:
167-
return false
168-
}
169-
}
170-
171100
extension Diagnostic.Message {
172101
static let addBlankLine = Diagnostic.Message(.warning, "add one blank line between declarations")
173-
174-
static func removeBlankLines(count: Int) -> Diagnostic.Message {
175-
let ending = count > 1 ? "s" : ""
176-
return Diagnostic.Message(.warning, "remove \(count) blank line\(ending)")
177-
}
178102
}

Tests/SwiftFormatRulesTests/BlankLineBetweenMembersTests.swift

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,52 @@ import SwiftSyntax
55
@testable import SwiftFormatRules
66

77
public class BlankLineBetweenMembersTests: DiagnosingTestCase {
8+
public func testBlankLineBeforeFirstChildOrNot() {
9+
XCTAssertFormatting(
10+
BlankLineBetweenMembers.self,
11+
input: """
12+
struct Foo {
13+
/// Doc comment
14+
func foo() {
15+
code()
16+
}
17+
}
18+
19+
struct Bar {
20+
21+
/// Doc comment
22+
func bar() {
23+
code()
24+
}
25+
}
26+
""",
27+
expected: """
28+
struct Foo {
29+
/// Doc comment
30+
func foo() {
31+
code()
32+
}
33+
}
34+
35+
struct Bar {
36+
37+
/// Doc comment
38+
func bar() {
39+
code()
40+
}
41+
}
42+
"""
43+
)
44+
}
45+
846
public func testInvalidBlankLineBetweenMembers() {
947
XCTAssertFormatting(
1048
BlankLineBetweenMembers.self,
1149
input: """
1250
struct foo1 {
13-
14-
15-
51+
52+
53+
1654
var test1 = 13
1755
// Multiline
1856
// comment for b
@@ -21,9 +59,9 @@ public class BlankLineBetweenMembersTests: DiagnosingTestCase {
2159
2260
2361
var c = 11
24-
2562
26-
63+
64+
2765
2866
// Multiline comment
2967
// for d
@@ -38,15 +76,23 @@ public class BlankLineBetweenMembersTests: DiagnosingTestCase {
3876
""",
3977
expected: """
4078
struct foo1 {
41-
79+
80+
81+
4282
var test1 = 13
83+
4384
// Multiline
4485
// comment for b
4586
var b = 12
87+
4688
/*BlockComment*/
47-
89+
90+
4891
var c = 11
4992
93+
94+
95+
5096
// Multiline comment
5197
// for d
5298
var d: Bool {
@@ -60,7 +106,7 @@ public class BlankLineBetweenMembersTests: DiagnosingTestCase {
60106
}
61107
""")
62108
}
63-
109+
64110
public func testTwoMembers() {
65111
XCTAssertFormatting(
66112
BlankLineBetweenMembers.self,
@@ -94,7 +140,7 @@ public class BlankLineBetweenMembersTests: DiagnosingTestCase {
94140
}
95141
""")
96142
}
97-
143+
98144
public func testNestedMembers() {
99145
XCTAssertFormatting(
100146
BlankLineBetweenMembers.self,
@@ -108,8 +154,6 @@ public class BlankLineBetweenMembersTests: DiagnosingTestCase {
108154
case jack, queen, king, ace
109155
}
110156
111-
112-
113157
struct secondFoo3 {
114158
var a = 1
115159
var e: Bool {
@@ -124,6 +168,7 @@ public class BlankLineBetweenMembersTests: DiagnosingTestCase {
124168
enum Rank: Int {
125169
case two = 2, three, four
126170
171+
127172
case jack, queen, king, ace
128173
}
129174

0 commit comments

Comments
 (0)