Skip to content

Commit 1427a1e

Browse files
committed
Add option to add a newline between 2 adjacent attributes
Add an option that inserts hard line breaks between adjacent attributes. Closes #773
1 parent b268009 commit 1427a1e

File tree

4 files changed

+123
-13
lines changed

4 files changed

+123
-13
lines changed

Sources/SwiftFormat/API/Configuration+Default.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ extension Configuration {
3030
self.lineBreakBeforeControlFlowKeywords = false
3131
self.lineBreakBeforeEachArgument = false
3232
self.lineBreakBeforeEachGenericRequirement = false
33+
self.lineBreakBetweenAttributes = false
3334
self.prioritizeKeepingFunctionOutputTogether = false
3435
self.indentConditionalCompilationBlocks = true
3536
self.lineBreakAroundMultilineExpressionChainComponents = false

Sources/SwiftFormat/API/Configuration.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public struct Configuration: Codable, Equatable {
3434
case lineBreakBeforeControlFlowKeywords
3535
case lineBreakBeforeEachArgument
3636
case lineBreakBeforeEachGenericRequirement
37+
case lineBreakBetweenAttributes
3738
case prioritizeKeepingFunctionOutputTogether
3839
case indentConditionalCompilationBlocks
3940
case lineBreakAroundMultilineExpressionChainComponents
@@ -111,6 +112,9 @@ public struct Configuration: Codable, Equatable {
111112
/// horizontally first, with line breaks only being fired when the line length would be exceeded.
112113
public var lineBreakBeforeEachGenericRequirement: Bool
113114

115+
/// If true, a line break will be added between adjacent attributes.
116+
public var lineBreakBetweenAttributes: Bool
117+
114118
/// Determines if function-like declaration outputs should be prioritized to be together with the
115119
/// function signature right (closing) parenthesis.
116120
///
@@ -243,6 +247,9 @@ public struct Configuration: Codable, Equatable {
243247
self.lineBreakBeforeEachGenericRequirement =
244248
try container.decodeIfPresent(Bool.self, forKey: .lineBreakBeforeEachGenericRequirement)
245249
?? defaults.lineBreakBeforeEachGenericRequirement
250+
self.lineBreakBetweenAttributes =
251+
try container.decodeIfPresent(Bool.self, forKey: .lineBreakBetweenAttributes)
252+
?? defaults.lineBreakBetweenAttributes
246253
self.prioritizeKeepingFunctionOutputTogether =
247254
try container.decodeIfPresent(Bool.self, forKey: .prioritizeKeepingFunctionOutputTogether)
248255
?? defaults.prioritizeKeepingFunctionOutputTogether
@@ -296,6 +303,7 @@ public struct Configuration: Codable, Equatable {
296303
try container.encode(lineBreakBeforeEachGenericRequirement, forKey: .lineBreakBeforeEachGenericRequirement)
297304
try container.encode(prioritizeKeepingFunctionOutputTogether, forKey: .prioritizeKeepingFunctionOutputTogether)
298305
try container.encode(indentConditionalCompilationBlocks, forKey: .indentConditionalCompilationBlocks)
306+
try container.encode(lineBreakBetweenAttributes, forKey: .lineBreakBetweenAttributes)
299307
try container.encode(
300308
lineBreakAroundMultilineExpressionChainComponents,
301309
forKey: .lineBreakAroundMultilineExpressionChainComponents)

Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
271271
// `arrange*` functions here.
272272
before(node.firstToken(viewMode: .sourceAccurate), tokens: .open)
273273

274-
arrangeAttributeList(node.attributes)
274+
arrangeAttributeList(node.attributes, separateByLineBreaks: config.lineBreakBeforeEachArgument)
275275

276276
let hasArguments = !node.signature.parameterClause.parameters.isEmpty
277277

@@ -326,7 +326,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
326326
) {
327327
before(node.firstToken(viewMode: .sourceAccurate), tokens: .open)
328328

329-
arrangeAttributeList(attributes)
329+
arrangeAttributeList(attributes, separateByLineBreaks: config.lineBreakBetweenAttributes)
330330

331331
// Prioritize keeping "<modifiers> <keyword> <name>:" together (corresponding group close is
332332
// below at `lastTokenBeforeBrace`).
@@ -458,7 +458,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
458458
after(node.returnClause.lastToken(viewMode: .sourceAccurate), tokens: .close)
459459
}
460460

461-
arrangeAttributeList(node.attributes)
461+
arrangeAttributeList(node.attributes, separateByLineBreaks: config.lineBreakBetweenAttributes)
462462

463463
if let genericWhereClause = node.genericWhereClause {
464464
before(genericWhereClause.firstToken(viewMode: .sourceAccurate), tokens: .break(.same), .open)
@@ -513,7 +513,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
513513
) where BodyContents.Element: SyntaxProtocol {
514514
before(node.firstToken(viewMode: .sourceAccurate), tokens: .open)
515515

516-
arrangeAttributeList(attributes)
516+
arrangeAttributeList(attributes, separateByLineBreaks: config.lineBreakBetweenAttributes)
517517
arrangeBracesAndContents(of: body, contentsKeyPath: bodyContentsKeyPath)
518518

519519
if let genericWhereClause = genericWhereClause {
@@ -549,7 +549,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
549549
}
550550

551551
override func visit(_ node: AccessorDeclSyntax) -> SyntaxVisitorContinueKind {
552-
arrangeAttributeList(node.attributes)
552+
arrangeAttributeList(node.attributes, separateByLineBreaks: config.lineBreakBetweenAttributes)
553553
arrangeBracesAndContents(of: node.body, contentsKeyPath: \.statements)
554554
return .visitChildren
555555
}
@@ -1327,7 +1327,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
13271327
}
13281328

13291329
override func visit(_ node: MacroExpansionDeclSyntax) -> SyntaxVisitorContinueKind {
1330-
arrangeAttributeList(node.attributes)
1330+
arrangeAttributeList(node.attributes, separateByLineBreaks: config.lineBreakBetweenAttributes)
13311331

13321332
before(
13331333
node.trailingClosure?.leftBrace,
@@ -1546,7 +1546,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
15461546
override func visit(_ node: EnumCaseDeclSyntax) -> SyntaxVisitorContinueKind {
15471547
before(node.firstToken(viewMode: .sourceAccurate), tokens: .open)
15481548

1549-
arrangeAttributeList(node.attributes)
1549+
arrangeAttributeList(node.attributes, separateByLineBreaks: config.lineBreakBetweenAttributes)
15501550

15511551
after(node.caseKeyword, tokens: .break)
15521552
after(node.lastToken(viewMode: .sourceAccurate), tokens: .close)
@@ -2179,7 +2179,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
21792179
}
21802180

21812181
override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
2182-
arrangeAttributeList(node.attributes)
2182+
arrangeAttributeList(node.attributes, separateByLineBreaks: config.lineBreakBetweenAttributes)
21832183

21842184
if node.bindings.count == 1 {
21852185
// If there is only a single binding, don't allow a break between the `let/var` keyword and
@@ -2285,7 +2285,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
22852285
}
22862286

22872287
override func visit(_ node: TypeAliasDeclSyntax) -> SyntaxVisitorContinueKind {
2288-
arrangeAttributeList(node.attributes)
2288+
arrangeAttributeList(node.attributes, separateByLineBreaks: config.lineBreakBetweenAttributes)
22892289

22902290
after(node.typealiasKeyword, tokens: .break)
22912291

@@ -2499,7 +2499,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
24992499
}
25002500

25012501
override func visit(_ node: AssociatedTypeDeclSyntax) -> SyntaxVisitorContinueKind {
2502-
arrangeAttributeList(node.attributes)
2502+
arrangeAttributeList(node.attributes, separateByLineBreaks: config.lineBreakBetweenAttributes)
25032503

25042504
after(node.associatedtypeKeyword, tokens: .break)
25052505

@@ -2890,14 +2890,16 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
28902890
/// Applies formatting tokens around and between the attributes in an attribute list.
28912891
private func arrangeAttributeList(
28922892
_ attributes: AttributeListSyntax?,
2893-
suppressFinalBreak: Bool = false
2893+
suppressFinalBreak: Bool = false,
2894+
separateByLineBreaks: Bool = false
28942895
) {
28952896
if let attributes = attributes {
2897+
let behavior: NewlineBehavior = separateByLineBreaks ? .hard : .elective
28962898
before(attributes.firstToken(viewMode: .sourceAccurate), tokens: .open)
2897-
insertTokens(.break(.same), betweenElementsOf: attributes)
2899+
insertTokens(.break(.same, newlines: behavior), betweenElementsOf: attributes)
28982900
var afterAttributeTokens = [Token.close]
28992901
if !suppressFinalBreak {
2900-
afterAttributeTokens.append(.break(.same))
2902+
afterAttributeTokens.append(.break(.same, newlines: behavior))
29012903
}
29022904
after(attributes.lastToken(viewMode: .sourceAccurate), tokens: afterAttributeTokens)
29032905
}

Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,4 +468,103 @@ final class AttributeTests: PrettyPrintTestCase {
468468

469469
assertPrettyPrintEqual(input: input, expected: expected, linelength: 100)
470470
}
471+
472+
func testLineBreakBetweenAttributes() {
473+
let input =
474+
"""
475+
@_spi(Private) @_spi(InviteOnly) import SwiftFormat
476+
477+
@available(iOS 14.0, *) @available(macOS 11.0, *)
478+
public protocol P {
479+
@available(iOS 16.0, *) @available(macOS 14.0, *)
480+
associatedtype ID
481+
}
482+
483+
@available(iOS 14.0, *) @available(macOS 11.0, *)
484+
public enum Dimension {
485+
case x
486+
case y
487+
@available(iOS 17.0, *) @available(visionOS 1.0, *)
488+
case z
489+
}
490+
491+
@available(iOS 16.0, *) @available(macOS 14.0, *)
492+
@available(tvOS 16.0, *) @frozen
493+
struct X {
494+
@available(iOS 17.0, *) @available(macOS 15.0, *)
495+
typealias ID = UUID
496+
497+
@available(iOS 17.0, *) @available(macOS 15.0, *)
498+
var callMe: @MainActor @Sendable () -> Void
499+
500+
@available(iOS 17.0, *) @available(macOS 15.0, *)
501+
@MainActor @discardableResult
502+
func f(@_inheritActorContext body: @MainActor @Sendable () -> Void) {}
503+
504+
@available(iOS 17.0, *) @available(macOS 15.0, *) @MainActor
505+
var foo: Foo {
506+
get { Foo() }
507+
@available(iOS, obsoleted: 17.0) @available(macOS 15.0, obsoleted: 15.0)
508+
set { fatalError() }
509+
}
510+
}
511+
"""
512+
513+
let expected =
514+
"""
515+
@_spi(Private) @_spi(InviteOnly) import SwiftFormat
516+
517+
@available(iOS 14.0, *)
518+
@available(macOS 11.0, *)
519+
public protocol P {
520+
@available(iOS 16.0, *)
521+
@available(macOS 14.0, *)
522+
associatedtype ID
523+
}
524+
525+
@available(iOS 14.0, *)
526+
@available(macOS 11.0, *)
527+
public enum Dimension {
528+
case x
529+
case y
530+
@available(iOS 17.0, *)
531+
@available(visionOS 1.0, *)
532+
case z
533+
}
534+
535+
@available(iOS 16.0, *)
536+
@available(macOS 14.0, *)
537+
@available(tvOS 16.0, *)
538+
@frozen
539+
struct X {
540+
@available(iOS 17.0, *)
541+
@available(macOS 15.0, *)
542+
typealias ID = UUID
543+
544+
@available(iOS 17.0, *)
545+
@available(macOS 15.0, *)
546+
var callMe: @MainActor @Sendable () -> Void
547+
548+
@available(iOS 17.0, *)
549+
@available(macOS 15.0, *)
550+
@MainActor
551+
@discardableResult
552+
func f(@_inheritActorContext body: @MainActor @Sendable () -> Void) {}
553+
554+
@available(iOS 17.0, *)
555+
@available(macOS 15.0, *)
556+
@MainActor
557+
var foo: Foo {
558+
get { Foo() }
559+
@available(iOS, obsoleted: 17.0)
560+
@available(macOS 15.0, obsoleted: 15.0)
561+
set { fatalError() }
562+
}
563+
}
564+
565+
"""
566+
var configuration = Configuration.forTesting
567+
configuration.lineBreakBetweenAttributes = true
568+
assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: configuration)
569+
}
471570
}

0 commit comments

Comments
 (0)