Skip to content

Commit e19ef9e

Browse files
authored
Merge pull request swiftlang#81 from dylansturg/fewer_line_breaks_in_properties
Update BlankLineBetweenMembers rule to remove false positives.
2 parents 1c01dee + 71eb322 commit e19ef9e

File tree

5 files changed

+156
-26
lines changed

5 files changed

+156
-26
lines changed

Sources/SwiftFormatConfiguration/Configuration.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,11 @@ public class Configuration: Codable {
159159
/// Configuration for the BlankLineBetweenMembers rule.
160160
public struct BlankLineBetweenMembersConfiguration: Codable {
161161
/// If true, blank lines are not required between single-line properties.
162-
public let ignoreSingleLineProperties = true
162+
public let ignoreSingleLineProperties: Bool
163+
164+
public init(ignoreSingleLineProperties: Bool = true) {
165+
self.ignoreSingleLineProperties = ignoreSingleLineProperties
166+
}
163167
}
164168

165169
/// Configuration for the NoPlaygroundLiterals rule.

Sources/SwiftFormatRules/BlankLineBetweenMembers.swift

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ import SwiftSyntax
3030
/// - SeeAlso: https://google.github.io/swift#vertical-whitespace
3131
public final class BlankLineBetweenMembers: SyntaxFormatRule {
3232
public override func visit(_ node: MemberDeclBlockSyntax) -> Syntax {
33-
guard let firstMember = node.members.first else { return super.visit(node) }
33+
// This rule is implemented by ensuring that all multiline member decls include a blank line
34+
// before the member's leading trivia and a blank line after any members that have a preceding
35+
// comment. Any comments that are separated from a member with blank lines are ignored, because
36+
// those comments aren't considered to be related to a specific member.
3437

38+
guard let firstMember = node.members.first else { return super.visit(node) }
3539
let ignoreSingleLine = context.configuration.blankLineBetweenMembers.ignoreSingleLineProperties
3640

3741
// The first member can just be added as-is; we don't force any newline before it.
@@ -40,45 +44,45 @@ public final class BlankLineBetweenMembers: SyntaxFormatRule {
4044
// The first comment may not be "attached" to the first member. Ignore any comments that are
4145
// separated from the member by a newline.
4246
let shouldIncludeLeadingComment = !isLeadingTriviaSeparate(from: firstMember)
43-
44-
// Iterates through all the declaration of the member, to ensure that the declarations have
45-
// at least one blank line between them when necessary.
4647
var previousMemberWasSingleLine = firstMember.isSingleLine(
4748
includingLeadingComment: shouldIncludeLeadingComment,
4849
sourceLocationConverter: context.sourceLocationConverter
4950
)
5051

52+
// Iterates through all the declaration of the member, to ensure that the declarations have
53+
// at least one blank line between them when necessary.
5154
for member in node.members.dropFirst() {
5255
var memberToAdd = visitNestedDecls(of: member)
5356

54-
let memberIsSingleLine = memberToAdd.isSingleLine(
57+
// Include the comment here to ensure all comments have a preceding blank line.
58+
let isMemberSingleLine = memberToAdd.isSingleLine(
5559
includingLeadingComment: true,
56-
sourceLocationConverter: context.sourceLocationConverter
57-
)
60+
sourceLocationConverter: context.sourceLocationConverter)
5861

59-
if let memberTrivia = memberToAdd.leadingTrivia,
60-
!(ignoreSingleLine && previousMemberWasSingleLine && memberIsSingleLine)
61-
&& memberTrivia.numberOfLeadingNewlines == 1 // Only one newline => no blank line
62-
{
63-
let correctTrivia = Trivia.newlines(1) + memberTrivia
62+
let ignoreMember = ignoreSingleLine && isMemberSingleLine
63+
if (!previousMemberWasSingleLine || !ignoreMember) && !isLeadingBlankLinePresent(on: member) {
64+
let blankLinePrefacedTrivia = Trivia.newlines(1) + (member.leadingTrivia ?? [])
6465
memberToAdd = replaceTrivia(
65-
on: memberToAdd, token: memberToAdd.firstToken!,
66-
leadingTrivia: correctTrivia
66+
on: memberToAdd,
67+
token: memberToAdd.firstToken,
68+
leadingTrivia: blankLinePrefacedTrivia
6769
) as! MemberDeclListItemSyntax
68-
6970
diagnose(.addBlankLine, on: member)
7071
}
7172

7273
membersList.append(memberToAdd)
73-
previousMemberWasSingleLine = memberIsSingleLine
74+
75+
// Consider the member single line if the trivia was separate so that non-member-specific
76+
// comments won't cause blank lines between members that are otherwise single line.
77+
previousMemberWasSingleLine = isMemberSingleLine || isLeadingTriviaSeparate(from: memberToAdd)
7478
}
7579

7680
return node.withMembers(SyntaxFactory.makeMemberDeclList(membersList))
7781
}
7882

7983
/// Returns whether any comments in the leading trivia of the given node are separated from the
8084
/// non-trivia tokens by at least 1 blank line.
81-
func isLeadingTriviaSeparate(from node: Syntax) -> Bool {
85+
private func isLeadingTriviaSeparate(from node: Syntax) -> Bool {
8286
guard let leadingTrivia = node.leadingTrivia else {
8387
return false
8488
}
@@ -88,6 +92,14 @@ public final class BlankLineBetweenMembers: SyntaxFormatRule {
8892
return false
8993
}
9094

95+
/// Returns whether there is at least 1 blank line in the leading trivia of the given node.
96+
private func isLeadingBlankLinePresent(on node: Syntax) -> Bool {
97+
guard let leadingTrivia = node.leadingTrivia else {
98+
return false
99+
}
100+
return leadingTrivia.numberOfLeadingNewlines > 1
101+
}
102+
91103
/// Recursively ensures all nested member types follows the BlankLineBetweenMembers rule.
92104
func visitNestedDecls(of member: MemberDeclListItemSyntax) -> MemberDeclListItemSyntax {
93105
switch member.decl {

Tests/SwiftFormatRulesTests/BlankLineBetweenMembersTests.swift

Lines changed: 113 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Foundation
2-
import XCTest
2+
import SwiftFormatConfiguration
33
import SwiftSyntax
4+
import XCTest
45

56
@testable import SwiftFormatRules
67

@@ -192,38 +193,146 @@ public class BlankLineBetweenMembersTests: DiagnosingTestCase {
192193
let baz = 2
193194
}
194195
enum Foo {
195-
196196
// MARK: - This is an important region of the code.
197197
198198
let bar = 1
199199
let baz = 2
200200
}
201201
enum Foo {
202+
var quxxe = 0
203+
// MARK: - This is an important region of the code.
202204
203-
// This comment is describing bar.
204205
let bar = 1
205206
let baz = 2
206207
}
208+
enum Foo {
209+
let bar = 1
210+
let baz = 2
211+
212+
// MARK: - This is an important region of the code.
213+
}
207214
""",
208215
expected: """
209216
enum Foo {
210217
let bar = 1
211218
let baz = 2
212219
}
213220
enum Foo {
221+
// MARK: - This is an important region of the code.
222+
223+
let bar = 1
224+
let baz = 2
225+
}
226+
enum Foo {
227+
var quxxe = 0
214228
215229
// MARK: - This is an important region of the code.
216230
217231
let bar = 1
218232
let baz = 2
219233
}
234+
enum Foo {
235+
let bar = 1
236+
let baz = 2
237+
238+
// MARK: - This is an important region of the code.
239+
}
240+
""")
241+
}
242+
243+
public func testBlankLinesAroundDocumentedMembers() {
244+
XCTAssertFormatting(
245+
BlankLineBetweenMembers.self,
246+
input: """
220247
enum Foo {
221248
222249
// This comment is describing bar.
223250
let bar = 1
251+
let baz = 2
252+
let quxxe = 3
253+
}
254+
enum Foo {
255+
var quxxe = 0
224256
257+
/// bar: A property that has a Bar.
258+
let bar = 1
225259
let baz = 2
260+
var car = 3
226261
}
227-
""")
262+
""",
263+
expected: """
264+
enum Foo {
265+
266+
// This comment is describing bar.
267+
let bar = 1
268+
269+
let baz = 2
270+
let quxxe = 3
271+
}
272+
enum Foo {
273+
var quxxe = 0
274+
275+
/// bar: A property that has a Bar.
276+
let bar = 1
277+
278+
let baz = 2
279+
var car = 3
280+
}
281+
""")
282+
}
283+
284+
public func testBlankLineBetweenMembersIgnoreSingleLineDisabled() {
285+
let config = Configuration()
286+
config.blankLineBetweenMembers =
287+
BlankLineBetweenMembersConfiguration(ignoreSingleLineProperties: false)
288+
289+
let input = """
290+
enum Foo {
291+
let bar = 1
292+
let baz = 2
293+
}
294+
enum Foo {
295+
// MARK: - This is an important region of the code.
296+
297+
let bar = 1
298+
let baz = 2
299+
}
300+
enum Foo {
301+
var quxxe = 0
302+
// MARK: - This is an important region of the code.
303+
304+
let bar = 1
305+
let baz = 2
306+
}
307+
"""
308+
let expected = """
309+
enum Foo {
310+
let bar = 1
311+
312+
let baz = 2
313+
}
314+
enum Foo {
315+
// MARK: - This is an important region of the code.
316+
317+
let bar = 1
318+
319+
let baz = 2
320+
}
321+
enum Foo {
322+
var quxxe = 0
323+
324+
// MARK: - This is an important region of the code.
325+
326+
let bar = 1
327+
328+
let baz = 2
329+
}
330+
"""
331+
332+
XCTAssertFormatting(
333+
BlankLineBetweenMembers.self,
334+
input: input,
335+
expected: expected,
336+
configuration: config)
228337
}
229338
}

Tests/SwiftFormatRulesTests/DiagnosingTestCase.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ public class DiagnosingTestCase: XCTestCase {
3838
}
3939

4040
/// Creates and returns a new `Context` from the given syntax tree.
41-
private func makeContext(sourceFileSyntax: SourceFileSyntax) -> Context {
41+
private func makeContext(sourceFileSyntax: SourceFileSyntax, configuration: Configuration? = nil)
42+
-> Context
43+
{
4244
let context = Context(
43-
configuration: Configuration(),
45+
configuration: configuration ?? Configuration(),
4446
diagnosticEngine: DiagnosticEngine(),
4547
fileURL: URL(fileURLWithPath: "/tmp/test.swift"),
4648
sourceFileSyntax: sourceFileSyntax)
@@ -101,7 +103,8 @@ public class DiagnosingTestCase: XCTestCase {
101103
expected: String,
102104
file: StaticString = #file,
103105
line: UInt = #line,
104-
checkForUnassertedDiagnostics: Bool = false
106+
checkForUnassertedDiagnostics: Bool = false,
107+
configuration: Configuration? = nil
105108
) {
106109
let sourceFileSyntax: SourceFileSyntax
107110
do {
@@ -111,7 +114,7 @@ public class DiagnosingTestCase: XCTestCase {
111114
return
112115
}
113116

114-
context = makeContext(sourceFileSyntax: sourceFileSyntax)
117+
context = makeContext(sourceFileSyntax: sourceFileSyntax, configuration: configuration)
115118

116119
// Force the rule to be enabled while we test it.
117120
context!.configuration.rules[formatType.ruleName] = true

Tests/SwiftFormatRulesTests/XCTestManifests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ extension BlankLineBetweenMembersTests {
4545
// to regenerate.
4646
static let __allTests__BlankLineBetweenMembersTests = [
4747
("testBlankLineBeforeFirstChildOrNot", testBlankLineBeforeFirstChildOrNot),
48+
("testBlankLineBetweenMembersIgnoreSingleLineDisabled", testBlankLineBetweenMembersIgnoreSingleLineDisabled),
49+
("testBlankLinesAroundDocumentedMembers", testBlankLinesAroundDocumentedMembers),
4850
("testInvalidBlankLineBetweenMembers", testInvalidBlankLineBetweenMembers),
4951
("testNestedMembers", testNestedMembers),
5052
("testNoBlankLineBetweenSingleLineMembers", testNoBlankLineBetweenSingleLineMembers),

0 commit comments

Comments
 (0)