diff --git a/Documentation/Configuration.md b/Documentation/Configuration.md index 1035286ce..d0d210bba 100644 --- a/Documentation/Configuration.md +++ b/Documentation/Configuration.md @@ -58,6 +58,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 diff --git a/Sources/SwiftFormat/API/Configuration+Default.swift b/Sources/SwiftFormat/API/Configuration+Default.swift index 3f0123fb4..be474fe3f 100644 --- a/Sources/SwiftFormat/API/Configuration+Default.swift +++ b/Sources/SwiftFormat/API/Configuration+Default.swift @@ -30,6 +30,7 @@ extension Configuration { self.lineBreakBeforeControlFlowKeywords = false self.lineBreakBeforeEachArgument = false self.lineBreakBeforeEachGenericRequirement = false + self.lineBreakBetweenDeclarationAttributes = false self.prioritizeKeepingFunctionOutputTogether = false self.indentConditionalCompilationBlocks = true self.lineBreakAroundMultilineExpressionChainComponents = false diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index ab8a3e952..89ab479e3 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -34,6 +34,7 @@ public struct Configuration: Codable, Equatable { case lineBreakBeforeControlFlowKeywords case lineBreakBeforeEachArgument case lineBreakBeforeEachGenericRequirement + case lineBreakBetweenDeclarationAttributes case prioritizeKeepingFunctionOutputTogether case indentConditionalCompilationBlocks case lineBreakAroundMultilineExpressionChainComponents @@ -111,6 +112,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. /// @@ -243,6 +247,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 @@ -296,6 +303,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) diff --git a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift index a2f6ccdf6..931875a20 100644 --- a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift @@ -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 @@ -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 " :" together (corresponding group close is // below at `lastTokenBeforeBrace`). @@ -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) @@ -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 { @@ -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 } @@ -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, @@ -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) @@ -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 @@ -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) @@ -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) @@ -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) } diff --git a/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift b/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift index 3ff5db02d..c16950390 100644 --- a/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift +++ b/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift @@ -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) + } }