Skip to content

Commit 3b05120

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 3b05120

File tree

5 files changed

+153
-13
lines changed

5 files changed

+153
-13
lines changed

Documentation/Configuration.md

+7
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ top-level keys and values:
5858
(the default), requirements will be laid out horizontally first, with line breaks
5959
only being fired when the line length would be exceeded.
6060

61+
* `lineBreakBetweenDeclartionAttributes` _(boolean)_: Determines the
62+
line-breaking behavior for adjacent attributes on declarations.
63+
If true, a line break will be added between each attribute, forcing the
64+
attribute list to be laid out vertically. If false (the default),
65+
attributes will be laid out horizontally first, with line breaks only
66+
being fired when the line length would be exceeded.
67+
6168
* `prioritizeKeepingFunctionOutputTogether` _(boolean)_: Determines if
6269
function-like declaration outputs should be prioritized to be together with the
6370
function signature right (closing) parenthesis. If false (the default), function

Sources/SwiftFormat/API/Configuration+Default.swift

+1
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.lineBreakBetweenDeclarationAttributes = false
3334
self.prioritizeKeepingFunctionOutputTogether = false
3435
self.indentConditionalCompilationBlocks = true
3536
self.lineBreakAroundMultilineExpressionChainComponents = false

Sources/SwiftFormat/API/Configuration.swift

+8
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 lineBreakBetweenDeclarationAttributes
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 lineBreakBetweenDeclarationAttributes: 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.lineBreakBetweenDeclarationAttributes =
251+
try container.decodeIfPresent(Bool.self, forKey: .lineBreakBetweenDeclarationAttributes)
252+
?? defaults.lineBreakBetweenDeclarationAttributes
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(lineBreakBetweenDeclarationAttributes, forKey: .lineBreakBetweenDeclarationAttributes)
299307
try container.encode(
300308
lineBreakAroundMultilineExpressionChainComponents,
301309
forKey: .lineBreakAroundMultilineExpressionChainComponents)

Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift

+29-13
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.lineBreakBetweenDeclarationAttributes)
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.lineBreakBetweenDeclarationAttributes)
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.lineBreakBetweenDeclarationAttributes)
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.lineBreakBetweenDeclarationAttributes)
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.lineBreakBetweenDeclarationAttributes)
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.lineBreakBetweenDeclarationAttributes)
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.lineBreakBetweenDeclarationAttributes)
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.lineBreakBetweenDeclarationAttributes)
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.lineBreakBetweenDeclarationAttributes)
25032503

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

@@ -2890,14 +2890,30 @@ 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+
for element in attributes.dropLast() {
2900+
if let ifConfig = element.as(IfConfigDeclSyntax.self) {
2901+
for clause in ifConfig.clauses {
2902+
if let nestedAttributes = AttributeListSyntax(clause.elements) {
2903+
arrangeAttributeList(
2904+
nestedAttributes,
2905+
suppressFinalBreak: true,
2906+
separateByLineBreaks: separateByLineBreaks
2907+
)
2908+
}
2909+
}
2910+
} else {
2911+
after(element.lastToken(viewMode: .sourceAccurate), tokens: .break(.same, newlines: behavior))
2912+
}
2913+
}
28982914
var afterAttributeTokens = [Token.close]
28992915
if !suppressFinalBreak {
2900-
afterAttributeTokens.append(.break(.same))
2916+
afterAttributeTokens.append(.break(.same, newlines: behavior))
29012917
}
29022918
after(attributes.lastToken(viewMode: .sourceAccurate), tokens: afterAttributeTokens)
29032919
}

Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift

+108
Original file line numberDiff line numberDiff line change
@@ -468,4 +468,112 @@ final class AttributeTests: PrettyPrintTestCase {
468468

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

0 commit comments

Comments
 (0)