diff --git a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift index db19638f7..c57bbef68 100644 --- a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift @@ -870,6 +870,11 @@ private final class TokenStreamCreator: SyntaxVisitor { } func visit(_ node: MemberDeclListSyntax) -> SyntaxVisitorContinueKind { + // This is the same as `insertTokens(_:betweenElementsOf:)`, but testing for an extra condition + // on the left-hand element. + for item in node.dropLast() where shouldInsertNewline(basedOn: item.semicolon) { + after(item.lastToken, tokens: .newline) + } return .visitChildren } @@ -962,6 +967,11 @@ private final class TokenStreamCreator: SyntaxVisitor { } func visit(_ node: CodeBlockItemListSyntax) -> SyntaxVisitorContinueKind { + // This is the same as `insertTokens(_:betweenElementsOf:)`, but testing for an extra condition + // on the left-hand element. + for item in node.dropLast() where shouldInsertNewline(basedOn: item.semicolon) { + after(item.lastToken, tokens: .newline) + } return .visitChildren } @@ -2183,6 +2193,21 @@ private final class TokenStreamCreator: SyntaxVisitor { return expression is ArrayExprSyntax || expression is DictionaryExprSyntax || expression is ClosureExprSyntax } + + /// Returns a value indicating whether a statement or member declaration should have a newline + /// inserted after it, based on the presence of a semicolon and whether or not the formatter is + /// respecting existing newlines. + private func shouldInsertNewline(basedOn semicolon: TokenSyntax?) -> Bool { + if config.respectsExistingLineBreaks { + // If we are respecting existing newlines, then we only want to force a newline at the end of + // statements and declarations that don't have a semicolon (i.e., where they are required). + return semicolon == nil + } else { + // If we are not respecting existing newlines, then we always force a newline (this forces + // even semicolon-delimited statements onto separate lines). + return true + } + } } extension Syntax { diff --git a/Tests/SwiftFormatPrettyPrintTests/RespectsExistingLineBreaksTests.swift b/Tests/SwiftFormatPrettyPrintTests/RespectsExistingLineBreaksTests.swift new file mode 100644 index 000000000..3c9d4ce8f --- /dev/null +++ b/Tests/SwiftFormatPrettyPrintTests/RespectsExistingLineBreaksTests.swift @@ -0,0 +1,198 @@ +import SwiftFormatConfiguration + +/// Sanity checks and regression tests for the `respectsExistingLineBreaks` configuration setting +/// in both true and false states. +class RespectsExistingLineBreaksTests: PrettyPrintTestCase { + func testExpressions() { + let input = + """ + a = b + c + + d + + e + f + g + + h + i + """ + + let expectedRespecting = + """ + a = b + c + + d + + e + f + + g + + h + i + + """ + + assertPrettyPrintEqual( + input: input, expected: expectedRespecting, linelength: 12, + configuration: configuration(respectingExistingLineBreaks: true)) + + let expectedNotRespecting = + """ + a = b + c + d + e + f + g + + h + i + + """ + + assertPrettyPrintEqual( + input: input, expected: expectedNotRespecting, linelength: 25, + configuration: configuration(respectingExistingLineBreaks: false)) + } + + func testCodeBlocksAndMemberDecls() { + let input = + """ + import Module + import Other + + struct FitsOnOneLine { + var x: Int + } + + struct Foo { + var storedProperty: Int + = 100 + var readOnlyProperty: Int { + return + 200 + } + var readWriteProperty: Int { + get { + return + somethingElse + } + set { + somethingElse + = newValue + } + } + + func oneLiner() -> Int { + return 500 + } + func someFunction( + x: Int + ) { + foo(x) + bar(x) + } + } + """ + + // No changes expected when respecting existing newlines. + assertPrettyPrintEqual( + input: input, expected: input + "\n", linelength: 80, + configuration: configuration(respectingExistingLineBreaks: true)) + + let expectedNotRespecting = + """ + import Module + import Other + + struct FitsOnOneLine { var x: Int } + + struct Foo { + var storedProperty: Int = 100 + var readOnlyProperty: Int { return 200 } + var readWriteProperty: Int { + get { return somethingElse } + set { somethingElse = newValue } + } + + func oneLiner() -> Int { return 500 } + func someFunction(x: Int) { + foo(x) + bar(x) + } + } + + """ + + assertPrettyPrintEqual( + input: input, expected: expectedNotRespecting, linelength: 80, + configuration: configuration(respectingExistingLineBreaks: false)) + } + + func testSemicolons() { + let input = + """ + foo(); bar(); + baz(); + + struct Foo { + var a: Int; var b: Int; + var c: Int; + } + """ + + // When respecting newlines, we should leave semicolon-delimited statements and declarations on + // the same line if they were originally like that and likewise preserve newlines after + // semicolons if present. + assertPrettyPrintEqual( + input: input, expected: input + "\n", linelength: 80, + configuration: configuration(respectingExistingLineBreaks: true)) + + let expectedNotRespecting = + """ + foo(); + bar(); + baz(); + + struct Foo { + var a: Int; + var b: Int; + var c: Int; + } + + """ + + // When not respecting newlines every semicolon-delimited statement or declaration should end up + // on its own line. + assertPrettyPrintEqual( + input: input, expected: expectedNotRespecting, linelength: 80, + configuration: configuration(respectingExistingLineBreaks: false)) + } + + func testInvalidBreaksAreAlwaysRejected() { + // Verify that newlines in places where a break would not be allowed are removed, regardless of + // the configuration setting. + let input = + """ + func foo + (bar + : Int) -> + Int { + return bar * 2 + } + """ + + let expectedRespecting = + """ + func foo(bar: Int) -> Int { + return bar * 2 + } + + """ + + assertPrettyPrintEqual( + input: input, expected: expectedRespecting, linelength: 80, + configuration: configuration(respectingExistingLineBreaks: true)) + + let expectedNotRespecting = + """ + func foo(bar: Int) -> Int { return bar * 2 } + + """ + + assertPrettyPrintEqual( + input: input, expected: expectedNotRespecting, linelength: 80, + configuration: configuration(respectingExistingLineBreaks: false)) + } + + /// Creates a new configuration with the given value for `respectsExistingLineBreaks` and default + /// values for everything else. + private func configuration(respectingExistingLineBreaks: Bool) -> Configuration { + let config = Configuration() + config.respectsExistingLineBreaks = respectingExistingLineBreaks + return config + } +} diff --git a/Tests/SwiftFormatPrettyPrintTests/XCTestManifests.swift b/Tests/SwiftFormatPrettyPrintTests/XCTestManifests.swift index 0345ccc4a..72fdfea92 100644 --- a/Tests/SwiftFormatPrettyPrintTests/XCTestManifests.swift +++ b/Tests/SwiftFormatPrettyPrintTests/XCTestManifests.swift @@ -396,6 +396,18 @@ extension RepeatStmtTests { ] } +extension RespectsExistingLineBreaksTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__RespectsExistingLineBreaksTests = [ + ("testCodeBlocksAndMemberDecls", testCodeBlocksAndMemberDecls), + ("testExpressions", testExpressions), + ("testInvalidBreaksAreAlwaysRejected", testInvalidBreaksAreAlwaysRejected), + ("testSemicolons", testSemicolons), + ] +} + extension SemiColonTypeTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -631,6 +643,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(OperatorDeclTests.__allTests__OperatorDeclTests), testCase(ProtocolDeclTests.__allTests__ProtocolDeclTests), testCase(RepeatStmtTests.__allTests__RepeatStmtTests), + testCase(RespectsExistingLineBreaksTests.__allTests__RespectsExistingLineBreaksTests), testCase(SemiColonTypeTests.__allTests__SemiColonTypeTests), testCase(SequenceExprFoldingTests.__allTests__SequenceExprFoldingTests), testCase(SomeTypeTests.__allTests__SomeTypeTests),