Skip to content

Commit

Permalink
Add option to add a newline between 2 adjacent attributes (#784)
Browse files Browse the repository at this point in the history
Add an option that inserts hard line breaks between adjacent attributes.
  • Loading branch information
dduan authored Aug 1, 2024
1 parent ffc641e commit 992d2c6
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 13 deletions.
7 changes: 7 additions & 0 deletions Documentation/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ top-level keys and values:
(the default), requirements will be laid out horizontally first, with line breaks
only being fired when the line length would be exceeded.

* `lineBreakBetweenDeclarationAttributes` _(boolean)_: Determines the
line-breaking behavior for adjacent attributes on declarations.
If true, a line break will be added between each attribute, forcing the
attribute list to be laid out vertically. If false (the default),
attributes will be laid out horizontally first, with line breaks only
being fired when the line length would be exceeded.

* `prioritizeKeepingFunctionOutputTogether` _(boolean)_: Determines if
function-like declaration outputs should be prioritized to be together with the
function signature right (closing) parenthesis. If false (the default), function
Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftFormat/API/Configuration+Default.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ extension Configuration {
self.lineBreakBeforeControlFlowKeywords = false
self.lineBreakBeforeEachArgument = false
self.lineBreakBeforeEachGenericRequirement = false
self.lineBreakBetweenDeclarationAttributes = false
self.prioritizeKeepingFunctionOutputTogether = false
self.indentConditionalCompilationBlocks = true
self.lineBreakAroundMultilineExpressionChainComponents = false
Expand Down
8 changes: 8 additions & 0 deletions Sources/SwiftFormat/API/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public struct Configuration: Codable, Equatable {
case lineBreakBeforeControlFlowKeywords
case lineBreakBeforeEachArgument
case lineBreakBeforeEachGenericRequirement
case lineBreakBetweenDeclarationAttributes
case prioritizeKeepingFunctionOutputTogether
case indentConditionalCompilationBlocks
case lineBreakAroundMultilineExpressionChainComponents
Expand Down Expand Up @@ -115,6 +116,9 @@ public struct Configuration: Codable, Equatable {
/// horizontally first, with line breaks only being fired when the line length would be exceeded.
public var lineBreakBeforeEachGenericRequirement: Bool

/// If true, a line break will be added between adjacent attributes.
public var lineBreakBetweenDeclarationAttributes: Bool

/// Determines if function-like declaration outputs should be prioritized to be together with the
/// function signature right (closing) parenthesis.
///
Expand Down Expand Up @@ -250,6 +254,9 @@ public struct Configuration: Codable, Equatable {
self.lineBreakBeforeEachGenericRequirement =
try container.decodeIfPresent(Bool.self, forKey: .lineBreakBeforeEachGenericRequirement)
?? defaults.lineBreakBeforeEachGenericRequirement
self.lineBreakBetweenDeclarationAttributes =
try container.decodeIfPresent(Bool.self, forKey: .lineBreakBetweenDeclarationAttributes)
?? defaults.lineBreakBetweenDeclarationAttributes
self.prioritizeKeepingFunctionOutputTogether =
try container.decodeIfPresent(Bool.self, forKey: .prioritizeKeepingFunctionOutputTogether)
?? defaults.prioritizeKeepingFunctionOutputTogether
Expand Down Expand Up @@ -304,6 +311,7 @@ public struct Configuration: Codable, Equatable {
try container.encode(lineBreakBeforeEachGenericRequirement, forKey: .lineBreakBeforeEachGenericRequirement)
try container.encode(prioritizeKeepingFunctionOutputTogether, forKey: .prioritizeKeepingFunctionOutputTogether)
try container.encode(indentConditionalCompilationBlocks, forKey: .indentConditionalCompilationBlocks)
try container.encode(lineBreakBetweenDeclarationAttributes, forKey: .lineBreakBetweenDeclarationAttributes)
try container.encode(
lineBreakAroundMultilineExpressionChainComponents,
forKey: .lineBreakAroundMultilineExpressionChainComponents)
Expand Down
42 changes: 29 additions & 13 deletions Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
// `arrange*` functions here.
before(node.firstToken(viewMode: .sourceAccurate), tokens: .open)

arrangeAttributeList(node.attributes)
arrangeAttributeList(node.attributes, separateByLineBreaks: config.lineBreakBeforeEachArgument)

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

Expand Down Expand Up @@ -326,7 +326,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
) {
before(node.firstToken(viewMode: .sourceAccurate), tokens: .open)

arrangeAttributeList(attributes)
arrangeAttributeList(attributes, separateByLineBreaks: config.lineBreakBetweenDeclarationAttributes)

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

arrangeAttributeList(node.attributes)
arrangeAttributeList(node.attributes, separateByLineBreaks: config.lineBreakBetweenDeclarationAttributes)

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

arrangeAttributeList(attributes)
arrangeAttributeList(attributes, separateByLineBreaks: config.lineBreakBetweenDeclarationAttributes)
arrangeBracesAndContents(of: body, contentsKeyPath: bodyContentsKeyPath)

if let genericWhereClause = genericWhereClause {
Expand Down Expand Up @@ -549,7 +549,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
}

override func visit(_ node: AccessorDeclSyntax) -> SyntaxVisitorContinueKind {
arrangeAttributeList(node.attributes)
arrangeAttributeList(node.attributes, separateByLineBreaks: config.lineBreakBetweenDeclarationAttributes)
arrangeBracesAndContents(of: node.body, contentsKeyPath: \.statements)
return .visitChildren
}
Expand Down Expand Up @@ -1327,7 +1327,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
}

override func visit(_ node: MacroExpansionDeclSyntax) -> SyntaxVisitorContinueKind {
arrangeAttributeList(node.attributes)
arrangeAttributeList(node.attributes, separateByLineBreaks: config.lineBreakBetweenDeclarationAttributes)

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

arrangeAttributeList(node.attributes)
arrangeAttributeList(node.attributes, separateByLineBreaks: config.lineBreakBetweenDeclarationAttributes)

after(node.caseKeyword, tokens: .break)
after(node.lastToken(viewMode: .sourceAccurate), tokens: .close)
Expand Down Expand Up @@ -2179,7 +2179,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
}

override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
arrangeAttributeList(node.attributes)
arrangeAttributeList(node.attributes, separateByLineBreaks: config.lineBreakBetweenDeclarationAttributes)

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

override func visit(_ node: TypeAliasDeclSyntax) -> SyntaxVisitorContinueKind {
arrangeAttributeList(node.attributes)
arrangeAttributeList(node.attributes, separateByLineBreaks: config.lineBreakBetweenDeclarationAttributes)

after(node.typealiasKeyword, tokens: .break)

Expand Down Expand Up @@ -2499,7 +2499,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
}

override func visit(_ node: AssociatedTypeDeclSyntax) -> SyntaxVisitorContinueKind {
arrangeAttributeList(node.attributes)
arrangeAttributeList(node.attributes, separateByLineBreaks: config.lineBreakBetweenDeclarationAttributes)

after(node.associatedtypeKeyword, tokens: .break)

Expand Down Expand Up @@ -2890,14 +2890,30 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
/// Applies formatting tokens around and between the attributes in an attribute list.
private func arrangeAttributeList(
_ attributes: AttributeListSyntax?,
suppressFinalBreak: Bool = false
suppressFinalBreak: Bool = false,
separateByLineBreaks: Bool = false
) {
if let attributes = attributes {
let behavior: NewlineBehavior = separateByLineBreaks ? .hard : .elective
before(attributes.firstToken(viewMode: .sourceAccurate), tokens: .open)
insertTokens(.break(.same), betweenElementsOf: attributes)
for element in attributes.dropLast() {
if let ifConfig = element.as(IfConfigDeclSyntax.self) {
for clause in ifConfig.clauses {
if let nestedAttributes = AttributeListSyntax(clause.elements) {
arrangeAttributeList(
nestedAttributes,
suppressFinalBreak: true,
separateByLineBreaks: separateByLineBreaks
)
}
}
} else {
after(element.lastToken(viewMode: .sourceAccurate), tokens: .break(.same, newlines: behavior))
}
}
var afterAttributeTokens = [Token.close]
if !suppressFinalBreak {
afterAttributeTokens.append(.break(.same))
afterAttributeTokens.append(.break(.same, newlines: behavior))
}
after(attributes.lastToken(viewMode: .sourceAccurate), tokens: afterAttributeTokens)
}
Expand Down
108 changes: 108 additions & 0 deletions Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -468,4 +468,112 @@ final class AttributeTests: PrettyPrintTestCase {

assertPrettyPrintEqual(input: input, expected: expected, linelength: 100)
}

func testLineBreakBetweenDeclarationAttributes() {
let input =
"""
@_spi(Private) @_spi(InviteOnly) import SwiftFormat
@available(iOS 14.0, *) @available(macOS 11.0, *)
public protocol P {
@available(iOS 16.0, *) @available(macOS 14.0, *)
#if DEBUG
@available(tvOS 17.0, *) @available(watchOS 10.0, *)
#endif
@available(visionOS 1.0, *)
associatedtype ID
}
@available(iOS 14.0, *) @available(macOS 11.0, *)
public enum Dimension {
case x
case y
@available(iOS 17.0, *) @available(visionOS 1.0, *)
case z
}
@available(iOS 16.0, *) @available(macOS 14.0, *)
@available(tvOS 16.0, *) @frozen
struct X {
@available(iOS 17.0, *) @available(macOS 15.0, *)
typealias ID = UUID
@available(iOS 17.0, *) @available(macOS 15.0, *)
var callMe: @MainActor @Sendable () -> Void
@available(iOS 17.0, *) @available(macOS 15.0, *)
@MainActor @discardableResult
func f(@_inheritActorContext body: @MainActor @Sendable () -> Void) {}
@available(iOS 17.0, *) @available(macOS 15.0, *) @MainActor
var foo: Foo {
get { Foo() }
@available(iOS, obsoleted: 17.0) @available(macOS 15.0, obsoleted: 15.0)
set { fatalError() }
}
}
"""

let expected =
"""
@_spi(Private) @_spi(InviteOnly) import SwiftFormat
@available(iOS 14.0, *)
@available(macOS 11.0, *)
public protocol P {
@available(iOS 16.0, *)
@available(macOS 14.0, *)
#if DEBUG
@available(tvOS 17.0, *)
@available(watchOS 10.0, *)
#endif
@available(visionOS 1.0, *)
associatedtype ID
}
@available(iOS 14.0, *)
@available(macOS 11.0, *)
public enum Dimension {
case x
case y
@available(iOS 17.0, *)
@available(visionOS 1.0, *)
case z
}
@available(iOS 16.0, *)
@available(macOS 14.0, *)
@available(tvOS 16.0, *)
@frozen
struct X {
@available(iOS 17.0, *)
@available(macOS 15.0, *)
typealias ID = UUID
@available(iOS 17.0, *)
@available(macOS 15.0, *)
var callMe: @MainActor @Sendable () -> Void
@available(iOS 17.0, *)
@available(macOS 15.0, *)
@MainActor
@discardableResult
func f(@_inheritActorContext body: @MainActor @Sendable () -> Void) {}
@available(iOS 17.0, *)
@available(macOS 15.0, *)
@MainActor
var foo: Foo {
get { Foo() }
@available(iOS, obsoleted: 17.0)
@available(macOS 15.0, obsoleted: 15.0)
set { fatalError() }
}
}
"""
var configuration = Configuration.forTesting
configuration.lineBreakBetweenDeclarationAttributes = true
assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: configuration)
}
}

0 comments on commit 992d2c6

Please sign in to comment.