diff --git a/.swift-format b/.swift-format index 41a022f26..176687e28 100644 --- a/.swift-format +++ b/.swift-format @@ -13,6 +13,8 @@ "NoBlockComments": false, "OrderedImports": true, "UseLetInEveryBoundCaseVariable": false, - "UseSynthesizedInitializer": false + "UseSynthesizedInitializer": false, + "ReturnVoidInsteadOfEmptyTuple": true, + "NoVoidReturnOnFunctionSignature": true, } } diff --git a/Sources/SwiftFormat/Core/Trivia+Convenience.swift b/Sources/SwiftFormat/Core/Trivia+Convenience.swift index c1906ab6c..138204231 100644 --- a/Sources/SwiftFormat/Core/Trivia+Convenience.swift +++ b/Sources/SwiftFormat/Core/Trivia+Convenience.swift @@ -80,13 +80,19 @@ extension Trivia { }) } - /// Returns `true` if this trivia contains any backslashes (used for multiline string newline - /// suppression). - var containsBackslashes: Bool { - return contains( - where: { - if case .backslashes = $0 { return true } - return false - }) + /// Returns the prefix of this trivia that corresponds to the backslash and pound signs used to + /// represent a non-line-break continuation of a multiline string, or nil if the trivia does not + /// represent such a continuation. + var multilineStringContinuation: String? { + var result = "" + for piece in pieces { + switch piece { + case .backslashes, .pounds: + piece.write(to: &result) + default: + break + } + } + return result.isEmpty ? nil : result } } diff --git a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift index 2a08bb15c..d5be22648 100644 --- a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift @@ -2385,6 +2385,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: AttributedTypeSyntax) -> SyntaxVisitorContinueKind { + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) arrangeAttributeList(node.attributes) for specifier in node.specifiers { after( @@ -2392,6 +2393,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { tokens: .break(.continue, newlines: .elective(ignoresDiscretionary: true)) ) } + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) return .visitChildren } @@ -2635,11 +2637,13 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { emitSegmentTextTokens(segmentText[...]) } - if node.trailingTrivia.containsBackslashes && !config.reflowMultilineStringLiterals.isAlways { - // Segments with trailing backslashes won't end with a literal newline; the backslash is - // considered trivia. To preserve the original text and wrapping, we need to manually render - // the backslash and a break into the token stream. - appendToken(.syntax("\\")) + if !config.reflowMultilineStringLiterals.isAlways, + let continuation = node.trailingTrivia.multilineStringContinuation + { + // Segments with trailing backslashes won't end with a literal newline; the backslash and any + // `#` delimiters for raw strings are considered trivia. To preserve the original text and + // wrapping, we need to manually render them break into the token stream. + appendToken(.syntax(continuation)) appendToken(.break(breakKind, newlines: .hard(count: 1))) } return .skipChildren diff --git a/Sources/swift-format/Frontend/FormatFrontend.swift b/Sources/swift-format/Frontend/FormatFrontend.swift index a205b6405..0e8fca9d2 100644 --- a/Sources/swift-format/Frontend/FormatFrontend.swift +++ b/Sources/swift-format/Frontend/FormatFrontend.swift @@ -40,7 +40,7 @@ class FormatFrontend: Frontend { return } - let diagnosticHandler: (SwiftDiagnostics.Diagnostic, SourceLocation) -> () = { + let diagnosticHandler: (SwiftDiagnostics.Diagnostic, SourceLocation) -> Void = { (diagnostic, location) in guard !self.lintFormatOptions.ignoreUnparsableFiles else { // No diagnostics should be emitted in this mode. diff --git a/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift b/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift index 4419d6f05..73950b917 100644 --- a/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift +++ b/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift @@ -623,4 +623,25 @@ final class AttributeTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 45) } + + func testAttributeLineBreakInInheritanceClause() { + let input = + """ + public class MyClass: Foo, @unchecked Sendable, Bar { + // … + } + + """ + let expected = + """ + public class MyClass: Foo, + @unchecked Sendable, Bar + { + // … + } + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 45) + } } diff --git a/Tests/SwiftFormatTests/PrettyPrint/FunctionDeclTests.swift b/Tests/SwiftFormatTests/PrettyPrint/FunctionDeclTests.swift index 08935ab19..1bfa566f1 100644 --- a/Tests/SwiftFormatTests/PrettyPrint/FunctionDeclTests.swift +++ b/Tests/SwiftFormatTests/PrettyPrint/FunctionDeclTests.swift @@ -1129,8 +1129,8 @@ final class FunctionDeclTests: PrettyPrintTestCase { .InnerMember, ) {} func foo( - cmp: @escaping (R) -> - () + cmp: + @escaping (R) -> () ) {} func foo( cmp: diff --git a/Tests/SwiftFormatTests/PrettyPrint/StringTests.swift b/Tests/SwiftFormatTests/PrettyPrint/StringTests.swift index 2a785bdb9..5b5172a28 100644 --- a/Tests/SwiftFormatTests/PrettyPrint/StringTests.swift +++ b/Tests/SwiftFormatTests/PrettyPrint/StringTests.swift @@ -739,4 +739,41 @@ final class StringTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: input + "\n", linelength: 20) } + + func testMultilineStringWithContinuations() { + let input = + ##""" + let someString = + """ + lines \ + \nare \ + short. + """ + let someString = + #""" + lines \# + \#nare \# + short. + """# + """## + + let expected = + ##""" + let someString = + """ + lines \ + \nare \ + short. + """ + let someString = + #""" + lines \# + \#nare \# + short. + """# + + """## + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 30) + } }