diff --git a/Sources/SwiftFormat/API/Configuration+Default.swift b/Sources/SwiftFormat/API/Configuration+Default.swift index 1af06a121..801e27b1d 100644 --- a/Sources/SwiftFormat/API/Configuration+Default.swift +++ b/Sources/SwiftFormat/API/Configuration+Default.swift @@ -42,5 +42,6 @@ extension Configuration { self.multiElementCollectionTrailingCommas = true self.reflowMultilineStringLiterals = .never self.indentBlankLines = false + self.alwaysBreakOnNewScopes = false } } diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index 70ac916aa..bc1024381 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -47,6 +47,7 @@ public struct Configuration: Codable, Equatable { case multiElementCollectionTrailingCommas case reflowMultilineStringLiterals case indentBlankLines + case alwaysBreakOnNewScopes } /// A dictionary containing the default enabled/disabled states of rules, keyed by the rules' @@ -268,6 +269,9 @@ public struct Configuration: Codable, Equatable { /// If false (the default), the whitespace in blank lines will be removed entirely. public var indentBlankLines: Bool + /// Determines whether to always break on new scopes. + public var alwaysBreakOnNewScopes: Bool + /// Creates a new `Configuration` by loading it from a configuration file. public init(contentsOf url: URL) throws { let data = try Data(contentsOf: url) @@ -383,6 +387,10 @@ public struct Configuration: Codable, Equatable { ) ?? defaults.indentBlankLines + self.alwaysBreakOnNewScopes = + try container.decodeIfPresent(Bool.self, forKey: .alwaysBreakOnNewScopes) + ?? false + // If the `rules` key is not present at all, default it to the built-in set // so that the behavior is the same as if the configuration had been // default-initialized. To get an empty rules dictionary, one can explicitly @@ -422,6 +430,7 @@ public struct Configuration: Codable, Equatable { try container.encode(multiElementCollectionTrailingCommas, forKey: .multiElementCollectionTrailingCommas) try container.encode(reflowMultilineStringLiterals, forKey: .reflowMultilineStringLiterals) try container.encode(rules, forKey: .rules) + try container.encode(alwaysBreakOnNewScopes, forKey: .alwaysBreakOnNewScopes) } /// Returns the URL of the configuration file that applies to the given file or directory. diff --git a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift index 03da7d4b4..2c0abd0e5 100644 --- a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift @@ -3226,20 +3226,27 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { ) where BodyContents.Element: SyntaxProtocol { guard let node = node, let contentsKeyPath = contentsKeyPath else { return } + let isEmpty = areBracesCompletelyEmpty(node, contentsKeyPath: contentsKeyPath) + let newlineBehavior: NewlineBehavior + if config.alwaysBreakOnNewScopes && !isEmpty { + // Force a newline after the left brace. + newlineBehavior = .hard(count: 1) + } else { + newlineBehavior = openBraceNewlineBehavior + } + if shouldResetBeforeLeftBrace { before( node.leftBrace, tokens: .break(.reset, size: 1, newlines: .elective(ignoresDiscretionary: true)) ) } - if !areBracesCompletelyEmpty(node, contentsKeyPath: contentsKeyPath) { - after( - node.leftBrace, - tokens: .break(.open, size: 1, newlines: openBraceNewlineBehavior), - .open - ) + + if !isEmpty { + after(node.leftBrace, tokens: .break(.open, size: 1, newlines: newlineBehavior), .open) before(node.rightBrace, tokens: .break(.close, size: 1), .close) } else { + // If empty scope, keep on the same line if allowed. after(node.leftBrace, tokens: .break(.open, size: 0, newlines: openBraceNewlineBehavior)) before(node.rightBrace, tokens: .break(.close, size: 0)) } diff --git a/Tests/SwiftFormatTests/PrettyPrint/AlwaysBreakOnNewScopesTests.swift b/Tests/SwiftFormatTests/PrettyPrint/AlwaysBreakOnNewScopesTests.swift new file mode 100644 index 000000000..39fe4cbce --- /dev/null +++ b/Tests/SwiftFormatTests/PrettyPrint/AlwaysBreakOnNewScopesTests.swift @@ -0,0 +1,91 @@ +import SwiftFormat + +final class AlwaysBreakOnNewScopesTests: PrettyPrintTestCase { + func testAlwaysBreakOnNewScopesEnabled() { + let input = + """ + class A { + func foo() -> Int { return 1 } + } + """ + + let expected = + """ + class A { + func foo() -> Int { + return 1 + } + } + + """ + var config = Configuration.forTesting + config.alwaysBreakOnNewScopes = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: config) + } + + func testAlwaysBreakOnNewScopesDisabled() { + let input = + """ + class A { + func foo() -> Int { return 1 } + } + """ + + let expected = + """ + class A { + func foo() -> Int { return 1 } + } + + """ + var config = Configuration.forTesting + config.alwaysBreakOnNewScopes = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: config) + } + + func testAlwaysBreakOnNewScopesUnlessScopeIsEmpty() { + let input = + """ + class A { + func foo() -> Int { } + } + """ + + let expected = + """ + class A { + func foo() -> Int {} + } + + """ + var config = Configuration.forTesting + config.alwaysBreakOnNewScopes = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: config) + } + + func testAlwaysBreakOnNewScopesNestedScopes() { + let input = + """ + class A { + func foo() -> Int { if true { 1 } else { 2 } } + } + """ + + let expected = + """ + class A { + func foo() -> Int { + if true { + 1 + } else { + 2 + } + } + } + + """ + var config = Configuration.forTesting + config.alwaysBreakOnNewScopes = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: config) + } +}