From a74fab584d2571c9d017bc9de1330124e09f2ed6 Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Mon, 10 Apr 2023 16:26:17 -0700 Subject: [PATCH 01/26] Merge pull request #501 from stackotter/fix-import-formatting Fix formatting of import with multiple attributes (fixes #445) and ensure that imports never wrap --- .../SwiftFormatPrettyPrint/PrettyPrint.swift | 21 ++++++++++++++----- Sources/SwiftFormatPrettyPrint/Token.swift | 6 ++++-- .../TokenStreamCreator.swift | 9 ++++++-- .../ImportTests.swift | 8 +++++++ 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/Sources/SwiftFormatPrettyPrint/PrettyPrint.swift b/Sources/SwiftFormatPrettyPrint/PrettyPrint.swift index a85173487..f35acc61b 100644 --- a/Sources/SwiftFormatPrettyPrint/PrettyPrint.swift +++ b/Sources/SwiftFormatPrettyPrint/PrettyPrint.swift @@ -129,11 +129,16 @@ public class PrettyPrinter { private var activeBreakSuppressionCount = 0 /// Whether breaks are supressed from firing. When true, no breaks should fire and the only way to - /// move to a new line is an explicit new line token. - private var isBreakingSupressed: Bool { + /// move to a new line is an explicit new line token. Discretionary breaks aren't suppressed + /// if ``allowSuppressedDiscretionaryBreaks`` is true. + private var isBreakingSuppressed: Bool { return activeBreakSuppressionCount > 0 } + /// Indicates whether discretionary breaks should still be included even if break suppression is + /// enabled (see ``isBreakingSuppressed``). + private var allowSuppressedDiscretionaryBreaks = false + /// The computed indentation level, as a number of spaces, based on the state of any unclosed /// delimiters and whether or not the current line is a continuation line. private var currentIndentation: [Indent] { @@ -469,7 +474,7 @@ public class PrettyPrinter { case .soft(_, let discretionary): // A discretionary newline (i.e. from the source) should create a line break even if the // rules for breaking are disabled. - overrideBreakingSuppressed = discretionary + overrideBreakingSuppressed = discretionary && allowSuppressedDiscretionaryBreaks mustBreak = true case .hard: // A hard newline must always create a line break, regardless of the context. @@ -477,7 +482,7 @@ public class PrettyPrinter { mustBreak = true } - let suppressBreaking = isBreakingSupressed && !overrideBreakingSuppressed + let suppressBreaking = isBreakingSuppressed && !overrideBreakingSuppressed if !suppressBreaking && ((!isAtStartOfLine && length > spaceRemaining) || mustBreak) { currentLineIsContinuation = isContinuationIfBreakFires writeNewlines(newline) @@ -527,8 +532,14 @@ public class PrettyPrinter { case .printerControl(let kind): switch kind { - case .disableBreaking: + case .disableBreaking(let allowDiscretionary): activeBreakSuppressionCount += 1 + // Override the supression of discretionary breaks if we're at the top level or + // discretionary breaks are currently allowed (false should override true, but not the other + // way around). + if activeBreakSuppressionCount == 1 || allowSuppressedDiscretionaryBreaks { + allowSuppressedDiscretionaryBreaks = allowDiscretionary + } case .enableBreaking: activeBreakSuppressionCount -= 1 } diff --git a/Sources/SwiftFormatPrettyPrint/Token.swift b/Sources/SwiftFormatPrettyPrint/Token.swift index 431edb288..fdfcb297f 100644 --- a/Sources/SwiftFormatPrettyPrint/Token.swift +++ b/Sources/SwiftFormatPrettyPrint/Token.swift @@ -162,8 +162,10 @@ enum PrinterControlKind { /// control token is encountered. /// /// It's valid to nest `disableBreaking` and `enableBreaking` tokens. Breaks will be suppressed - /// long as there is at least 1 unmatched disable token. - case disableBreaking + /// long as there is at least 1 unmatched disable token. If `allowDiscretionary` is `true`, then + /// discretionary breaks aren't effected. An `allowDiscretionary` value of true never overrides a + /// value of false. Hard breaks are always inserted no matter what. + case disableBreaking(allowDiscretionary: Bool) /// A signal that break tokens should be allowed to fire following this token, as long as there /// are no other unmatched disable tokens. diff --git a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift index 78a7834bd..ba957be9a 100644 --- a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift @@ -1394,7 +1394,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { ] ) } else if let condition = node.condition { - before(condition.firstToken, tokens: .printerControl(kind: .disableBreaking)) + before(condition.firstToken, tokens: .printerControl(kind: .disableBreaking(allowDiscretionary: true))) after( condition.lastToken, tokens: .printerControl(kind: .enableBreaking), .break(.reset, size: 0)) @@ -1723,9 +1723,14 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind { - after(node.attributes?.lastToken, tokens: .space) + // Import declarations should never be wrapped. + before(node.firstToken, tokens: .printerControl(kind: .disableBreaking(allowDiscretionary: false))) + + arrangeAttributeList(node.attributes) after(node.importTok, tokens: .space) after(node.importKind, tokens: .space) + + after(node.lastToken, tokens: .printerControl(kind: .enableBreaking)) return .visitChildren } diff --git a/Tests/SwiftFormatPrettyPrintTests/ImportTests.swift b/Tests/SwiftFormatPrettyPrintTests/ImportTests.swift index befa073b5..326f36718 100644 --- a/Tests/SwiftFormatPrettyPrintTests/ImportTests.swift +++ b/Tests/SwiftFormatPrettyPrintTests/ImportTests.swift @@ -7,6 +7,12 @@ final class ImportTests: PrettyPrintTestCase { import class MyModule.MyClass import struct MyModule.MyStruct @testable import testModule + + @_spi( + STP + ) + @testable + import testModule """ let expected = @@ -17,6 +23,8 @@ final class ImportTests: PrettyPrintTestCase { import struct MyModule.MyStruct @testable import testModule + @_spi(STP) @testable import testModule + """ // Imports should not wrap From ce7db95b7da1244ca313efd3c3c1b7f05d29284c Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Mon, 10 Apr 2023 16:27:11 -0700 Subject: [PATCH 02/26] Merge pull request #499 from stackotter/issue-473 Fix fatal error caused by switch cases without any statements (#473) --- .../TokenStreamCreator.swift | 16 ++++++-- .../SwitchStmtTests.swift | 37 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift index ba957be9a..3f1ee5ec8 100644 --- a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift @@ -718,13 +718,23 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { after(node.unknownAttr?.lastToken, tokens: .space) after(node.label.lastToken, tokens: .break(.reset, size: 0), .break(.open), .open) - // If switch/case labels were configured to be indented, insert an extra `close` break after the - // case body to match the `open` break above + // If switch/case labels were configured to be indented, insert an extra `close` break after + // the case body to match the `open` break above var afterLastTokenTokens: [Token] = [.break(.close, size: 0), .close] if config.indentSwitchCaseLabels { afterLastTokenTokens.append(.break(.close, size: 0)) } - after(node.lastToken, tokens: afterLastTokenTokens) + + // If the case contains statements, add the closing tokens after the last token of the case. + // Otherwise, add the closing tokens before the next case (or the end of the switch) to have the + // same effect. If instead the opening and closing tokens were omitted completely in the absence + // of statements, comments within the empty case would be incorrectly indented to the same level + // as the case label. + if node.label.lastToken != node.lastToken { + after(node.lastToken, tokens: afterLastTokenTokens) + } else { + before(node.nextToken, tokens: afterLastTokenTokens) + } return .visitChildren } diff --git a/Tests/SwiftFormatPrettyPrintTests/SwitchStmtTests.swift b/Tests/SwiftFormatPrettyPrintTests/SwitchStmtTests.swift index 35726d224..08e07d94e 100644 --- a/Tests/SwiftFormatPrettyPrintTests/SwitchStmtTests.swift +++ b/Tests/SwiftFormatPrettyPrintTests/SwitchStmtTests.swift @@ -78,6 +78,43 @@ final class SwitchStmtTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 35) } + func testSwitchEmptyCases() { + let input = + """ + switch a { + case b: + default: + print("Not b") + } + + switch a { + case b: + // Comment but no statements + default: + print("Not b") + } + """ + + let expected = + """ + switch a { + case b: + default: + print("Not b") + } + + switch a { + case b: + // Comment but no statements + default: + print("Not b") + } + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 35) + } + func testSwitchCompoundCases() { let input = """ From 65c863666a029360c1e98135e0a34126c36ffbbe Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Mon, 10 Apr 2023 19:24:17 -0700 Subject: [PATCH 03/26] Merge pull request #509 from allevato/remove-tsc Remove the `swift-tools-support-core` dependency from swift-format. --- Package.swift | 6 - .../Frontend/FormatFrontend.swift | 3 +- Sources/swift-format/Frontend/Frontend.swift | 4 +- .../swift-format/Utilities/Diagnostic.swift | 80 ++++++++++ .../Utilities/DiagnosticsEngine.swift | 126 +++++++++++++++ .../Utilities/StderrDiagnosticPrinter.swift | 28 ++-- Sources/swift-format/Utilities/TTY.swift | 29 ++++ .../Utilities/UnifiedDiagnosticsEngine.swift | 149 ------------------ 8 files changed, 254 insertions(+), 171 deletions(-) create mode 100644 Sources/swift-format/Utilities/Diagnostic.swift create mode 100644 Sources/swift-format/Utilities/DiagnosticsEngine.swift create mode 100644 Sources/swift-format/Utilities/TTY.swift delete mode 100644 Sources/swift-format/Utilities/UnifiedDiagnosticsEngine.swift diff --git a/Package.swift b/Package.swift index d7912810e..e7448ec41 100644 --- a/Package.swift +++ b/Package.swift @@ -143,7 +143,6 @@ let package = Package( .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftParser", package: "swift-syntax"), - .product(name: "TSCBasic", package: "swift-tools-support-core"), ] ), @@ -219,15 +218,10 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { url: "https://github.com/apple/swift-syntax.git", branch: "release/5.9" ), - .package( - url: "https://github.com/apple/swift-tools-support-core.git", - exact: Version("0.4.0") - ), ] } else { package.dependencies += [ .package(path: "../swift-argument-parser"), .package(path: "../swift-syntax"), - .package(path: "../swift-tools-support-core"), ] } diff --git a/Sources/swift-format/Frontend/FormatFrontend.swift b/Sources/swift-format/Frontend/FormatFrontend.swift index 996b1a924..aac36856b 100644 --- a/Sources/swift-format/Frontend/FormatFrontend.swift +++ b/Sources/swift-format/Frontend/FormatFrontend.swift @@ -40,7 +40,8 @@ class FormatFrontend: Frontend { return } - let diagnosticHandler: (Diagnostic, SourceLocation) -> () = { (diagnostic, location) in + let diagnosticHandler: (SwiftDiagnostics.Diagnostic, SourceLocation) -> () = { + (diagnostic, location) in guard !self.lintFormatOptions.ignoreUnparsableFiles else { // No diagnostics should be emitted in this mode. return diff --git a/Sources/swift-format/Frontend/Frontend.swift b/Sources/swift-format/Frontend/Frontend.swift index fa9611fac..0c8c4a50f 100644 --- a/Sources/swift-format/Frontend/Frontend.swift +++ b/Sources/swift-format/Frontend/Frontend.swift @@ -57,7 +57,7 @@ class Frontend { final let diagnosticPrinter: StderrDiagnosticPrinter /// The diagnostic engine to which warnings and errors will be emitted. - final let diagnosticsEngine: UnifiedDiagnosticsEngine + final let diagnosticsEngine: DiagnosticsEngine /// Options that apply during formatting or linting. final let lintFormatOptions: LintFormatOptions @@ -83,7 +83,7 @@ class Frontend { self.diagnosticPrinter = StderrDiagnosticPrinter( colorMode: lintFormatOptions.colorDiagnostics.map { $0 ? .on : .off } ?? .auto) self.diagnosticsEngine = - UnifiedDiagnosticsEngine(diagnosticsHandlers: [diagnosticPrinter.printDiagnostic]) + DiagnosticsEngine(diagnosticsHandlers: [diagnosticPrinter.printDiagnostic]) } /// Runs the linter or formatter over the inputs. diff --git a/Sources/swift-format/Utilities/Diagnostic.swift b/Sources/swift-format/Utilities/Diagnostic.swift new file mode 100644 index 000000000..3a80333a9 --- /dev/null +++ b/Sources/swift-format/Utilities/Diagnostic.swift @@ -0,0 +1,80 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftFormatCore +import SwiftSyntax + +/// Diagnostic data that retains the separation of a finding category (if present) from the rest of +/// the message, allowing diagnostic printers that want to print those values separately to do so. +struct Diagnostic { + /// The severity of the diagnostic. + enum Severity { + case note + case warning + case error + } + + /// Represents the location of a diagnostic. + struct Location { + /// The file path associated with the diagnostic. + var file: String + + /// The 1-based line number where the diagnostic occurred. + var line: Int + + /// The 1-based column number where the diagnostic occurred. + var column: Int + + /// Creates a new diagnostic location from the given source location. + init(_ sourceLocation: SourceLocation) { + self.file = sourceLocation.file! + self.line = sourceLocation.line! + self.column = sourceLocation.column! + } + + /// Creates a new diagnostic location with the given finding location. + init(_ findingLocation: Finding.Location) { + self.file = findingLocation.file + self.line = findingLocation.line + self.column = findingLocation.column + } + } + + /// The severity of the diagnostic. + var severity: Severity + + /// The location where the diagnostic occurred, if known. + var location: Location? + + /// The category of the diagnostic, if any. + var category: String? + + /// The message text associated with the diagnostic. + var message: String + + var description: String { + if let category = category { + return "[\(category)] \(message)" + } else { + return message + } + } + + /// Creates a new diagnostic with the given severity, location, optional category, and + /// message. + init(severity: Severity, location: Location?, category: String? = nil, message: String) { + self.severity = severity + self.location = location + self.category = category + self.message = message + } +} diff --git a/Sources/swift-format/Utilities/DiagnosticsEngine.swift b/Sources/swift-format/Utilities/DiagnosticsEngine.swift new file mode 100644 index 000000000..52e0b8909 --- /dev/null +++ b/Sources/swift-format/Utilities/DiagnosticsEngine.swift @@ -0,0 +1,126 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftFormatCore +import SwiftSyntax +import SwiftDiagnostics + +/// Unifies the handling of findings from the linter, parsing errors from the syntax parser, and +/// generic errors from the frontend so that they are emitted in a uniform fashion. +final class DiagnosticsEngine { + /// The handler functions that will be called to process diagnostics that are emitted. + private let handlers: [(Diagnostic) -> Void] + + /// A Boolean value indicating whether any errors were emitted by the diagnostics engine. + private(set) var hasErrors: Bool + + /// A Boolean value indicating whether any warnings were emitted by the diagnostics engine. + private(set) var hasWarnings: Bool + + /// Creates a new diagnostics engine with the given diagnostic handlers. + /// + /// - Parameter diagnosticsHandlers: An array of functions, each of which takes a `Diagnostic` as + /// its sole argument and returns `Void`. The functions are called whenever a diagnostic is + /// received by the engine. + init(diagnosticsHandlers: [(Diagnostic) -> Void]) { + self.handlers = diagnosticsHandlers + self.hasErrors = false + self.hasWarnings = false + } + + /// Emits the diagnostic by passing it to the registered handlers, and tracks whether it was an + /// error or warning diagnostic. + private func emit(_ diagnostic: Diagnostic) { + switch diagnostic.severity { + case .error: self.hasErrors = true + case .warning: self.hasWarnings = true + default: break + } + + for handler in handlers { + handler(diagnostic) + } + } + + /// Emits a generic error message. + /// + /// - Parameters: + /// - message: The message associated with the error. + /// - location: The location in the source code associated with the error, or nil if there is no + /// location associated with the error. + func emitError(_ message: String, location: SourceLocation? = nil) { + emit( + Diagnostic( + severity: .error, + location: location.map(Diagnostic.Location.init), + message: message)) + } + + /// Emits a finding from the linter and any of its associated notes as diagnostics. + /// + /// - Parameter finding: The finding that should be emitted. + func consumeFinding(_ finding: Finding) { + emit(diagnosticMessage(for: finding)) + + for note in finding.notes { + emit( + Diagnostic( + severity: .note, + location: note.location.map(Diagnostic.Location.init), + message: "\(note.message)")) + } + } + + /// Emits a diagnostic from the syntax parser and any of its associated notes. + /// + /// - Parameter diagnostic: The syntax parser diagnostic that should be emitted. + func consumeParserDiagnostic( + _ diagnostic: SwiftDiagnostics.Diagnostic, + _ location: SourceLocation + ) { + emit(diagnosticMessage(for: diagnostic.diagMessage, at: location)) + } + + /// Converts a diagnostic message from the syntax parser into a diagnostic message that can be + /// used by the `TSCBasic` diagnostics engine and returns it. + private func diagnosticMessage( + for message: SwiftDiagnostics.DiagnosticMessage, + at location: SourceLocation + ) -> Diagnostic { + let severity: Diagnostic.Severity + switch message.severity { + case .error: severity = .error + case .warning: severity = .warning + case .note: severity = .note + } + return Diagnostic( + severity: severity, + location: Diagnostic.Location(location), + category: nil, + message: message.message) + } + + /// Converts a lint finding into a diagnostic message that can be used by the `TSCBasic` + /// diagnostics engine and returns it. + private func diagnosticMessage(for finding: Finding) -> Diagnostic { + let severity: Diagnostic.Severity + switch finding.severity { + case .error: severity = .error + case .warning: severity = .warning + } + return Diagnostic( + severity: severity, + location: finding.location.map(Diagnostic.Location.init), + category: "\(finding.category)", + message: "\(finding.message.text)") + } +} diff --git a/Sources/swift-format/Utilities/StderrDiagnosticPrinter.swift b/Sources/swift-format/Utilities/StderrDiagnosticPrinter.swift index f6452be82..f7730f00c 100644 --- a/Sources/swift-format/Utilities/StderrDiagnosticPrinter.swift +++ b/Sources/swift-format/Utilities/StderrDiagnosticPrinter.swift @@ -12,7 +12,6 @@ import Dispatch import Foundation -import TSCBasic /// Manages printing of diagnostics to standard error. final class StderrDiagnosticPrinter { @@ -49,11 +48,7 @@ final class StderrDiagnosticPrinter { init(colorMode: ColorMode) { switch colorMode { case .auto: - if let stream = stderrStream.stream as? LocalFileOutputByteStream { - useColors = TerminalController.isTTY(stream) - } else { - useColors = false - } + useColors = isTTY(FileHandle.standardError) case .off: useColors = false case .on: @@ -62,25 +57,32 @@ final class StderrDiagnosticPrinter { } /// Prints a diagnostic to standard error. - func printDiagnostic(_ diagnostic: TSCBasic.Diagnostic) { + func printDiagnostic(_ diagnostic: Diagnostic) { printQueue.sync { let stderr = FileHandleTextOutputStream(FileHandle.standardError) - stderr.write("\(ansiSGR(.boldWhite))\(diagnostic.location): ") + stderr.write("\(ansiSGR(.boldWhite))\(description(of: diagnostic.location)): ") - switch diagnostic.behavior { + switch diagnostic.severity { case .error: stderr.write("\(ansiSGR(.boldRed))error: ") case .warning: stderr.write("\(ansiSGR(.boldMagenta))warning: ") case .note: stderr.write("\(ansiSGR(.boldGray))note: ") - case .remark, .ignored: break } - let data = diagnostic.data as! UnifiedDiagnosticData - if let category = data.category { + if let category = diagnostic.category { stderr.write("\(ansiSGR(.boldYellow))[\(category)] ") } - stderr.write("\(ansiSGR(.boldWhite))\(data.message)\(ansiSGR(.reset))\n") + stderr.write("\(ansiSGR(.boldWhite))\(diagnostic.message)\(ansiSGR(.reset))\n") + } + } + + /// Returns a string representation of the given diagnostic location, or a fallback string if the + /// location was not known. + private func description(of location: Diagnostic.Location?) -> String { + if let location = location { + return "\(location.file):\(location.line):\(location.column)" } + return "" } /// Returns the complete ANSI sequence used to enable the given SGR if colors are enabled in the diff --git a/Sources/swift-format/Utilities/TTY.swift b/Sources/swift-format/Utilities/TTY.swift new file mode 100644 index 000000000..35fc35841 --- /dev/null +++ b/Sources/swift-format/Utilities/TTY.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Foundation + +/// Returns a value indicating whether or not the stream is a TTY. +func isTTY(_ fileHandle: FileHandle) -> Bool { + // The implementation of this function is adapted from `TerminalController.swift` in + // swift-tools-support-core. + #if os(Windows) + // The TSC implementation of this function only returns `.file` or `.dumb` for Windows, + // neither of which is a TTY. + return false + #else + if ProcessInfo.processInfo.environment["TERM"] == "dumb" { + return false + } + return isatty(fileHandle.fileDescriptor) != 0 + #endif +} diff --git a/Sources/swift-format/Utilities/UnifiedDiagnosticsEngine.swift b/Sources/swift-format/Utilities/UnifiedDiagnosticsEngine.swift deleted file mode 100644 index ccce0cb41..000000000 --- a/Sources/swift-format/Utilities/UnifiedDiagnosticsEngine.swift +++ /dev/null @@ -1,149 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import SwiftFormatCore -import SwiftSyntax -import SwiftDiagnostics -import TSCBasic - -/// Diagnostic data that retains the separation of a finding category (if present) from the rest of -/// the message, allowing diagnostic printers that want to print those values separately to do so. -struct UnifiedDiagnosticData: DiagnosticData { - /// The category of the diagnostic, if any. - var category: String? - - /// The message text associated with the diagnostic. - var message: String - - var description: String { - if let category = category { - return "[\(category)] \(message)" - } else { - return message - } - } - - /// Creates a new unified diagnostic with the given optional category and message. - init(category: String? = nil, message: String) { - self.category = category - self.message = message - } -} - -/// Unifies the handling of findings from the linter, parsing errors from the syntax parser, and -/// generic errors from the frontend so that they are treated uniformly by the underlying -/// diagnostics engine from the `swift-tools-support-core` package. -final class UnifiedDiagnosticsEngine { - /// Represents a location from either the linter or the syntax parser and supports converting it - /// to a string representation for printing. - private enum UnifiedLocation: DiagnosticLocation { - /// A location received from the swift parser. - case parserLocation(SourceLocation) - - /// A location received from the linter. - case findingLocation(Finding.Location) - - var description: String { - switch self { - case .parserLocation(let location): - return "\(location.file):\(location.line):\(location.column)" - case .findingLocation(let location): - return "\(location.file):\(location.line):\(location.column)" - } - } - } - - /// The underlying diagnostics engine. - private let diagnosticsEngine: DiagnosticsEngine - - /// A Boolean value indicating whether any errors were emitted by the diagnostics engine. - var hasErrors: Bool { diagnosticsEngine.hasErrors } - - /// A Boolean value indicating whether any warnings were emitted by the diagnostics engine. - var hasWarnings: Bool { - diagnosticsEngine.diagnostics.contains { $0.behavior == .warning } - } - - /// Creates a new unified diagnostics engine with the given diagnostic handlers. - /// - /// - Parameter diagnosticsHandlers: An array of functions, each of which takes a `Diagnostic` as - /// its sole argument and returns `Void`. The functions are called whenever a diagnostic is - /// received by the engine. - init(diagnosticsHandlers: [DiagnosticsEngine.DiagnosticsHandler]) { - self.diagnosticsEngine = DiagnosticsEngine(handlers: diagnosticsHandlers) - } - - /// Emits a generic error message. - /// - /// - Parameters: - /// - message: The message associated with the error. - /// - location: The location in the source code associated with the error, or nil if there is no - /// location associated with the error. - func emitError(_ message: String, location: SourceLocation? = nil) { - diagnosticsEngine.emit( - .error(UnifiedDiagnosticData(message: message)), - location: location.map(UnifiedLocation.parserLocation)) - } - - /// Emits a finding from the linter and any of its associated notes as diagnostics. - /// - /// - Parameter finding: The finding that should be emitted. - func consumeFinding(_ finding: Finding) { - diagnosticsEngine.emit( - diagnosticMessage(for: finding), - location: finding.location.map(UnifiedLocation.findingLocation)) - - for note in finding.notes { - diagnosticsEngine.emit( - .note(UnifiedDiagnosticData(message: "\(note.message)")), - location: note.location.map(UnifiedLocation.findingLocation)) - } - } - - /// Emits a diagnostic from the syntax parser and any of its associated notes. - /// - /// - Parameter diagnostic: The syntax parser diagnostic that should be emitted. - func consumeParserDiagnostic( - _ diagnostic: SwiftDiagnostics.Diagnostic, - _ location: SourceLocation - ) { - diagnosticsEngine.emit( - diagnosticMessage(for: diagnostic.diagMessage), - location: UnifiedLocation.parserLocation(location)) - } - - /// Converts a diagnostic message from the syntax parser into a diagnostic message that can be - /// used by the `TSCBasic` diagnostics engine and returns it. - private func diagnosticMessage(for message: SwiftDiagnostics.DiagnosticMessage) - -> TSCBasic.Diagnostic.Message - { - let data = UnifiedDiagnosticData(category: nil, message: message.message) - - switch message.severity { - case .error: return .error(data) - case .warning: return .warning(data) - case .note: return .note(data) - } - } - - /// Converts a lint finding into a diagnostic message that can be used by the `TSCBasic` - /// diagnostics engine and returns it. - private func diagnosticMessage(for finding: Finding) -> TSCBasic.Diagnostic.Message { - let data = - UnifiedDiagnosticData(category: "\(finding.category)", message: "\(finding.message.text)") - - switch finding.severity { - case .error: return .error(data) - case .warning: return .warning(data) - } - } -} From 8d7619576e9ee0a49b004c6aafcd851806324f6f Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Thu, 20 Apr 2023 09:00:19 -0700 Subject: [PATCH 04/26] Merge pull request #513 from kimdv/kimdv/remove-ComputedLocation [SwiftSyntax] Remove force unwrapping for source location --- Sources/swift-format/Utilities/Diagnostic.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/swift-format/Utilities/Diagnostic.swift b/Sources/swift-format/Utilities/Diagnostic.swift index 3a80333a9..42305eab9 100644 --- a/Sources/swift-format/Utilities/Diagnostic.swift +++ b/Sources/swift-format/Utilities/Diagnostic.swift @@ -36,9 +36,9 @@ struct Diagnostic { /// Creates a new diagnostic location from the given source location. init(_ sourceLocation: SourceLocation) { - self.file = sourceLocation.file! - self.line = sourceLocation.line! - self.column = sourceLocation.column! + self.file = sourceLocation.file + self.line = sourceLocation.line + self.column = sourceLocation.column } /// Creates a new diagnostic location with the given finding location. From fe958d1a8b909fdc79a3ba3f1b05a39586a11baf Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Tue, 11 Apr 2023 07:25:53 -0700 Subject: [PATCH 05/26] Merge pull request #498 from stackotter/issue-494 Fix pretty printing of multi-statement closures (issue #494) --- .../TokenStreamCreator.swift | 2 +- .../ClosureExprTests.swift | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift index 3f1ee5ec8..fef274eab 100644 --- a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift @@ -1083,7 +1083,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind { let newlineBehavior: NewlineBehavior - if forcedBreakingClosures.remove(node.id) != nil { + if forcedBreakingClosures.remove(node.id) != nil || node.statements.count > 1 { newlineBehavior = .soft } else { newlineBehavior = .elective diff --git a/Tests/SwiftFormatPrettyPrintTests/ClosureExprTests.swift b/Tests/SwiftFormatPrettyPrintTests/ClosureExprTests.swift index 5518c5a96..cab0b8553 100644 --- a/Tests/SwiftFormatPrettyPrintTests/ClosureExprTests.swift +++ b/Tests/SwiftFormatPrettyPrintTests/ClosureExprTests.swift @@ -516,4 +516,24 @@ final class ClosureExprTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 40) } + + func testClosureWithSignatureAndMultipleStatements() { + let input = + """ + { a in a + 1 + a + 2 + } + """ + + let expected = + """ + { a in + a + 1 + a + 2 + } + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40) + } } From c0a9fa2ad96ecea3d706f90079293f561d36bf95 Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Tue, 11 Apr 2023 09:27:33 -0700 Subject: [PATCH 06/26] Merge pull request #503 from stackotter/issue-298 Avoid removing certain disambiguating parens from conditions (fixes #298) --- .../NoParensAroundConditions.swift | 14 ++++++++++---- .../NoParensAroundConditionsTests.swift | 13 +++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftFormatRules/NoParensAroundConditions.swift b/Sources/SwiftFormatRules/NoParensAroundConditions.swift index 162715254..1ee8678a7 100644 --- a/Sources/SwiftFormatRules/NoParensAroundConditions.swift +++ b/Sources/SwiftFormatRules/NoParensAroundConditions.swift @@ -30,10 +30,16 @@ public final class NoParensAroundConditions: SyntaxFormatRule { assert(tuple.elementList.count == 1) let expr = tuple.elementList.first!.expression - // If the condition is a function with a trailing closure, removing the - // outer set of parentheses introduces a parse ambiguity. - if let fnCall = expr.as(FunctionCallExprSyntax.self), fnCall.trailingClosure != nil { - return ExprSyntax(tuple) + // If the condition is a function with a trailing closure or if it's an immediately called + // closure, removing the outer set of parentheses introduces a parse ambiguity. + if let fnCall = expr.as(FunctionCallExprSyntax.self) { + if fnCall.trailingClosure != nil { + // Leave parentheses around call with trailing closure. + return ExprSyntax(tuple) + } else if fnCall.calledExpression.as(ClosureExprSyntax.self) != nil { + // Leave parentheses around immediately called closure. + return ExprSyntax(tuple) + } } diagnose(.removeParensAroundExpression, on: expr) diff --git a/Tests/SwiftFormatRulesTests/NoParensAroundConditionsTests.swift b/Tests/SwiftFormatRulesTests/NoParensAroundConditionsTests.swift index 865082ed3..9dbe9d244 100644 --- a/Tests/SwiftFormatRulesTests/NoParensAroundConditionsTests.swift +++ b/Tests/SwiftFormatRulesTests/NoParensAroundConditionsTests.swift @@ -160,4 +160,17 @@ final class NoParensAroundConditionsTests: LintOrFormatRuleTestCase { } """) } + + func testParensAroundAmbiguousConditions() { + XCTAssertFormatting( + NoParensAroundConditions.self, + input: """ + if ({ true }()) {} + if (functionWithTrailingClosure { 5 }) {} + """, + expected: """ + if ({ true }()) {} + if (functionWithTrailingClosure { 5 }) {} + """) + } } From ae0f3fd58a72373f2e3d0448ae561c9c1202b79b Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Tue, 11 Apr 2023 12:57:41 -0700 Subject: [PATCH 07/26] Merge pull request #402 from DavidBrunow/fixMorePostfixPoundIfs Fix more postfix pound if scenarios --- .../TokenStreamCreator.swift | 51 +++++++-- .../IfConfigTests.swift | 108 ++++++++++++++---- 2 files changed, 129 insertions(+), 30 deletions(-) diff --git a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift index fef274eab..82e11f2f9 100644 --- a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift @@ -1396,11 +1396,24 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } if isNestedInPostfixIfConfig(node: Syntax(node)) { + let breakToken: Token + let currentIfConfigDecl = node.parent?.parent?.as(IfConfigDeclSyntax.self) + + if let currentIfConfigDecl = currentIfConfigDecl, + let tokenBeforeCurrentIfConfigDecl = currentIfConfigDecl.previousToken, + isNestedInIfConfig(node: Syntax(tokenBeforeCurrentIfConfigDecl)) || + tokenBeforeCurrentIfConfigDecl.text == "}" { + breakToken = .break(.reset) + } else { + breakToken = .break + before(currentIfConfigDecl?.poundEndif, tokens: [.break]) + } + before( node.firstToken, tokens: [ .printerControl(kind: .enableBreaking), - .break(.reset), + breakToken, ] ) } else if let condition = node.condition { @@ -3567,17 +3580,39 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } private func isNestedInPostfixIfConfig(node: Syntax) -> Bool { - var this: Syntax? = node + var this: Syntax? = node - while this?.parent != nil { - if this?.parent?.is(PostfixIfConfigExprSyntax.self) == true { - return true - } + while this?.parent != nil { + // This guard handles the situation where a type with its own modifiers + // is nested inside of an if config. That type should not count as being + // in a postfix if config because its entire body is inside the if config. + if this?.is(TupleExprElementSyntax.self) == true { + return false + } - this = this?.parent + if this?.is(IfConfigDeclSyntax.self) == true && + this?.parent?.is(PostfixIfConfigExprSyntax.self) == true { + return true } - return false + this = this?.parent + } + + return false +} + +private func isNestedInIfConfig(node: Syntax) -> Bool { + var this: Syntax? = node + + while this?.parent != nil { + if this?.is(IfConfigClauseSyntax.self) == true { + return true + } + + this = this?.parent + } + + return false } extension Syntax { diff --git a/Tests/SwiftFormatPrettyPrintTests/IfConfigTests.swift b/Tests/SwiftFormatPrettyPrintTests/IfConfigTests.swift index 9547816f0..9a01fe631 100644 --- a/Tests/SwiftFormatPrettyPrintTests/IfConfigTests.swift +++ b/Tests/SwiftFormatPrettyPrintTests/IfConfigTests.swift @@ -247,10 +247,10 @@ final class IfConfigTests: PrettyPrintTestCase { """ VStack { Text("something") - #if os(iOS) - .iOSSpecificModifier() - #endif - .commonModifier() + #if os(iOS) + .iOSSpecificModifier() + #endif + .commonModifier() } """ @@ -277,13 +277,13 @@ final class IfConfigTests: PrettyPrintTestCase { """ VStack { Text("something") - #if os(iOS) - .iOSSpecificModifier() - .anotherModifier() - .anotherAnotherModifier() - #endif - .commonModifier() - .anotherCommonModifier() + #if os(iOS) + .iOSSpecificModifier() + .anotherModifier() + .anotherAnotherModifier() + #endif + .commonModifier() + .anotherCommonModifier() } """ @@ -311,14 +311,14 @@ final class IfConfigTests: PrettyPrintTestCase { """ VStack { Text("something") - #if os(iOS) || os(watchOS) - #if os(iOS) - .iOSModifier() - #else - .watchOSModifier() + #if os(iOS) || os(watchOS) + #if os(iOS) + .iOSModifier() + #else + .watchOSModifier() + #endif + .iOSAndWatchOSModifier() #endif - .iOSAndWatchOSModifier() - #endif } """ @@ -343,10 +343,10 @@ final class IfConfigTests: PrettyPrintTestCase { """ VStack { textView - #if os(iOS) - .iOSSpecificModifier() - #endif - .commonModifier() + #if os(iOS) + .iOSSpecificModifier() + #endif + .commonModifier() } """ @@ -390,4 +390,68 @@ final class IfConfigTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 45) } + + func testPostfixPoundIfBetweenOtherModifiers() { + let input = + """ + EmptyView() + .padding([.vertical]) + #if os(iOS) + .iOSSpecificModifier() + #endif + .commonModifier() + """ + + let expected = + """ + EmptyView() + .padding([.vertical]) + #if os(iOS) + .iOSSpecificModifier() + #endif + .commonModifier() + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 45) + } + + func testPostfixPoundIfWithTypeInModifier() { + let input = + """ + EmptyView() + .padding([.vertical]) + #if os(iOS) + .iOSSpecificModifier( + SpecificType() + .onChanged { _ in + // do things + } + .onEnded { _ in + // do things + } + ) + #endif + """ + + let expected = + """ + EmptyView() + .padding([.vertical]) + #if os(iOS) + .iOSSpecificModifier( + SpecificType() + .onChanged { _ in + // do things + } + .onEnded { _ in + // do things + } + ) + #endif + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 45) + } } From 6e6b38b55a3e8d3d5f4f03683c1dafec32eb8918 Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Wed, 12 Apr 2023 10:25:24 -0700 Subject: [PATCH 08/26] Merge pull request #510 from CippoX/warning-sourceaccurate Removed warnings --- .../LegacyTriviaBehavior.swift | 2 +- Sources/SwiftFormatCore/RuleMask.swift | 8 +- .../TokenStreamCreator.swift | 408 +++++++++--------- .../AddModifierRewriter.swift | 6 +- .../DeclSyntaxProtocol+Comments.swift | 2 +- .../SwiftFormatRules/DoNotUseSemicolons.swift | 2 +- .../SwiftFormatRules/FullyIndirectEnum.swift | 6 +- .../ModifierListSyntax+Convenience.swift | 4 +- .../NoCasesWithOnlyFallthrough.swift | 2 +- .../NoEmptyTrailingClosureParentheses.swift | 4 +- .../NoParensAroundConditions.swift | 2 +- .../OneVariableDeclarationPerLine.swift | 2 +- Sources/SwiftFormatRules/OrderedImports.swift | 10 +- .../ReturnVoidInsteadOfEmptyTuple.swift | 4 +- .../UseShorthandTypeNames.swift | 10 +- ...eTripleSlashForDocumentationComments.swift | 2 +- .../UseWhereClausesInForLoops.swift | 2 +- .../ValidateDocumentationComments.swift | 2 +- 18 files changed, 239 insertions(+), 239 deletions(-) diff --git a/Sources/SwiftFormatCore/LegacyTriviaBehavior.swift b/Sources/SwiftFormatCore/LegacyTriviaBehavior.swift index 5011c8be8..6fc9365c3 100644 --- a/Sources/SwiftFormatCore/LegacyTriviaBehavior.swift +++ b/Sources/SwiftFormatCore/LegacyTriviaBehavior.swift @@ -20,7 +20,7 @@ private final class LegacyTriviaBehaviorRewriter: SyntaxRewriter { token = token.with(\.leadingTrivia, pendingLeadingTrivia + token.leadingTrivia) self.pendingLeadingTrivia = nil } - if token.nextToken != nil, + if token.nextToken(viewMode: .sourceAccurate) != nil, let firstIndexToMove = token.trailingTrivia.firstIndex(where: shouldTriviaPieceBeMoved) { pendingLeadingTrivia = Trivia(pieces: Array(token.trailingTrivia[firstIndexToMove...])) diff --git a/Sources/SwiftFormatCore/RuleMask.swift b/Sources/SwiftFormatCore/RuleMask.swift index b614b17b0..37dfec1c2 100644 --- a/Sources/SwiftFormatCore/RuleMask.swift +++ b/Sources/SwiftFormatCore/RuleMask.swift @@ -136,7 +136,7 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor { // MARK: - Syntax Visitation Methods override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind { - guard let firstToken = node.firstToken else { + guard let firstToken = node.firstToken(viewMode: .sourceAccurate) else { return .visitChildren } let comments = loneLineComments(in: firstToken.leadingTrivia, isFirstToken: true) @@ -159,14 +159,14 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor { } override func visit(_ node: CodeBlockItemSyntax) -> SyntaxVisitorContinueKind { - guard let firstToken = node.firstToken else { + guard let firstToken = node.firstToken(viewMode: .sourceAccurate) else { return .visitChildren } return appendRuleStatusDirectives(from: firstToken, of: Syntax(node)) } override func visit(_ node: MemberDeclListItemSyntax) -> SyntaxVisitorContinueKind { - guard let firstToken = node.firstToken else { + guard let firstToken = node.firstToken(viewMode: .sourceAccurate) else { return .visitChildren } return appendRuleStatusDirectives(from: firstToken, of: Syntax(node)) @@ -183,7 +183,7 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor { private func appendRuleStatusDirectives(from token: TokenSyntax, of node: Syntax) -> SyntaxVisitorContinueKind { - let isFirstInFile = token.previousToken == nil + let isFirstInFile = token.previousToken(viewMode: .sourceAccurate) == nil let matches = loneLineComments(in: token.leadingTrivia, isFirstToken: isFirstInFile) .compactMap(ruleStatusDirectiveMatch) let sourceRange = node.sourceRange(converter: sourceLocationConverter) diff --git a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift index 82e11f2f9..358b3301e 100644 --- a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift @@ -109,7 +109,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { betweenElementsOf collectionNode: Node ) where Node.Element == Syntax { for element in collectionNode.dropLast() { - after(element.lastToken, tokens: tokens) + after(element.lastToken(viewMode: .sourceAccurate), tokens: tokens) } } @@ -120,7 +120,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { betweenElementsOf collectionNode: Node ) where Node.Element: SyntaxProtocol { for element in collectionNode.dropLast() { - after(element.lastToken, tokens: tokens) + after(element.lastToken(viewMode: .sourceAccurate), tokens: tokens) } } @@ -131,18 +131,18 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { betweenElementsOf collectionNode: Node ) where Node.Element == DeclSyntax { for element in collectionNode.dropLast() { - after(element.lastToken, tokens: tokens) + after(element.lastToken(viewMode: .sourceAccurate), tokens: tokens) } } private func verbatimToken(_ node: Syntax, indentingBehavior: IndentingBehavior = .allLines) { - if let firstToken = node.firstToken { + if let firstToken = node.firstToken(viewMode: .sourceAccurate) { appendBeforeTokens(firstToken) } appendToken(.verbatim(Verbatim(text: node.description, indentingBehavior: indentingBehavior))) - if let lastToken = node.lastToken { + if let lastToken = node.lastToken(viewMode: .sourceAccurate) { // Extract any comments that trail the verbatim block since they belong to the next syntax // token. Leading comments don't need special handling since they belong to the current node, // and will get printed. @@ -224,7 +224,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { - guard let lastTokenOfExtendedType = node.extendedType.lastToken else { + guard let lastTokenOfExtendedType = node.extendedType.lastToken(viewMode: .sourceAccurate) else { fatalError("ExtensionDeclSyntax.extendedType must have at least one token") } arrangeTypeDeclBlock( @@ -253,29 +253,29 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { genericWhereClause: GenericWhereClauseSyntax?, memberBlock: MemberDeclBlockSyntax ) { - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) arrangeAttributeList(attributes) // Prioritize keeping " :" together (corresponding group close is // below at `lastTokenBeforeBrace`). - let firstTokenAfterAttributes = modifiers?.firstToken ?? typeKeyword + let firstTokenAfterAttributes = modifiers?.firstToken(viewMode: .sourceAccurate) ?? typeKeyword before(firstTokenAfterAttributes, tokens: .open) after(typeKeyword, tokens: .break) arrangeBracesAndContents(of: memberBlock, contentsKeyPath: \.members) if let genericWhereClause = genericWhereClause { - before(genericWhereClause.firstToken, tokens: .break(.same), .open) - after(memberBlock.leftBrace, tokens: .close) + before(genericWhereClause.firstToken(viewMode: .sourceAccurate), tokens: .break(.same), .open) + after(members.leftBrace, tokens: .close) } let lastTokenBeforeBrace = inheritanceClause?.colon - ?? genericParameterOrPrimaryAssociatedTypeClause?.lastToken + ?? genericParameterOrPrimaryAssociatedTypeClause?.lastToken(viewMode: .sourceAccurate) ?? identifier after(lastTokenBeforeBrace, tokens: .close) - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) } // MARK: - Function and function-like declaration nodes (initializers, deinitializers, subscripts) @@ -287,7 +287,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // has arguments. if hasArguments && config.prioritizeKeepingFunctionOutputTogether { // Due to visitation order, the matching .open break is added in ParameterClauseSyntax. - after(node.signature.lastToken, tokens: .close) + after(node.signature.lastToken(viewMode: .sourceAccurate), tokens: .close) } let mustBreak = node.body != nil || node.signature.output != nil @@ -295,7 +295,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // Prioritize keeping " func (" together. Also include the ")" if the parameter // list is empty. - let firstTokenAfterAttributes = node.modifiers?.firstToken ?? node.funcKeyword + let firstTokenAfterAttributes = node.modifiers?.firstToken(viewMode: .sourceAccurate) ?? node.funcKeyword before(firstTokenAfterAttributes, tokens: .open) after(node.funcKeyword, tokens: .break) if hasArguments || node.genericParameterClause != nil { @@ -309,7 +309,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // defining a prefix or postfix operator, the token kind always comes through as // `binaryOperator`. if case .binaryOperator = node.identifier.tokenKind { - after(node.identifier.lastToken, tokens: .space) + after(node.identifier.lastToken(viewMode: .sourceAccurate), tokens: .space) } arrangeFunctionLikeDecl( @@ -329,13 +329,13 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // has arguments. if hasArguments && config.prioritizeKeepingFunctionOutputTogether { // Due to visitation order, the matching .open break is added in ParameterClauseSyntax. - after(node.signature.lastToken, tokens: .close) + after(node.signature.lastToken(viewMode: .sourceAccurate), tokens: .close) } arrangeParameterClause(node.signature.input, forcesBreakBeforeRightParen: node.body != nil) // Prioritize keeping " init" together. - let firstTokenAfterAttributes = node.modifiers?.firstToken ?? node.initKeyword + let firstTokenAfterAttributes = node.modifiers?.firstToken(viewMode: .sourceAccurate) ?? node.initKeyword before(firstTokenAfterAttributes, tokens: .open) if hasArguments || node.genericParameterClause != nil { @@ -367,10 +367,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: SubscriptDeclSyntax) -> SyntaxVisitorContinueKind { let hasArguments = !node.indices.parameterList.isEmpty - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) // Prioritize keeping " subscript" together. - if let firstModifierToken = node.modifiers?.firstToken { + if let firstModifierToken = node.modifiers?.firstToken(viewMode: .sourceAccurate) { before(firstModifierToken, tokens: .open) if hasArguments || node.genericParameterClause != nil { @@ -384,17 +384,17 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // arguments. if hasArguments && config.prioritizeKeepingFunctionOutputTogether { // Due to visitation order, the matching .open break is added in ParameterClauseSyntax. - after(node.result.lastToken, tokens: .close) + after(node.result.lastToken(viewMode: .sourceAccurate), tokens: .close) } arrangeAttributeList(node.attributes) if let genericWhereClause = node.genericWhereClause { - before(genericWhereClause.firstToken, tokens: .break(.same), .open) - after(genericWhereClause.lastToken, tokens: .close) + before(genericWhereClause.firstToken(viewMode: .sourceAccurate), tokens: .break(.same), .open) + after(genericWhereClause.lastToken(viewMode: .sourceAccurate), tokens: .close) } - before(node.result.firstToken, tokens: .break) + before(node.result.firstToken(viewMode: .sourceAccurate), tokens: .break) if let accessorOrCodeBlock = node.accessor { switch accessorOrCodeBlock { @@ -405,7 +405,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } } - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) arrangeParameterClause(node.indices, forcesBreakBeforeRightParen: true) @@ -421,17 +421,17 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { body: Node?, bodyContentsKeyPath: KeyPath? ) where BodyContents.Element: SyntaxProtocol { - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) arrangeAttributeList(attributes) arrangeBracesAndContents(of: body, contentsKeyPath: bodyContentsKeyPath) if let genericWhereClause = genericWhereClause { - before(genericWhereClause.firstToken, tokens: .break(.same), .open) - after(body?.leftBrace ?? genericWhereClause.lastToken, tokens: .close) + before(genericWhereClause.firstToken(viewMode: .sourceAccurate), tokens: .break(.same), .open) + after(body?.leftBrace ?? genericWhereClause.lastToken(viewMode: .sourceAccurate), tokens: .close) } - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) } // MARK: - Property and subscript accessor block nodes @@ -442,7 +442,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // protocol and we want to let them be placed on the same line if possible. Otherwise, we // place a newline between each accessor. let newlines: NewlineBehavior = child.body == nil ? .elective : .soft - after(child.lastToken, tokens: .break(.same, size: 1, newlines: newlines)) + after(child.lastToken(viewMode: .sourceAccurate), tokens: .break(.same, size: 1, newlines: newlines)) } return .visitChildren } @@ -484,8 +484,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // There may be a consistent breaking group around this node, see `CodeBlockItemSyntax`. This // group is necessary so that breaks around and inside of the conditions aren't forced to break // when the if-stmt spans multiple lines. - before(node.conditions.firstToken, tokens: .open) - after(node.conditions.lastToken, tokens: .close) + before(node.conditions.firstToken(viewMode: .sourceAccurate), tokens: .open) + after(node.conditions.lastToken(viewMode: .sourceAccurate), tokens: .close) after(node.ifKeyword, tokens: .space) @@ -494,8 +494,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // the conditions. There are no breaks around the first condition because if-statements look // better without a break between the "if" and the first condition. for condition in node.conditions.dropFirst() { - before(condition.firstToken, tokens: .break(.open(kind: .continuation), size: 0)) - after(condition.lastToken, tokens: .break(.close(mustBreak: false), size: 0)) + before(condition.firstToken(viewMode: .sourceAccurate), tokens: .break(.open(kind: .continuation), size: 0)) + after(condition.lastToken(viewMode: .sourceAccurate), tokens: .break(.close(mustBreak: false), size: 0)) } arrangeBracesAndContents(of: node.body, contentsKeyPath: \.statements) @@ -531,8 +531,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // Add break groups, using open continuation breaks, around all conditions so that continuations // inside of the conditions can stack in addition to continuations between the conditions. for condition in node.conditions { - before(condition.firstToken, tokens: .break(.open(kind: .continuation), size: 0)) - after(condition.lastToken, tokens: .break(.close(mustBreak: false), size: 0)) + before(condition.firstToken(viewMode: .sourceAccurate), tokens: .break(.open(kind: .continuation), size: 0)) + after(condition.lastToken(viewMode: .sourceAccurate), tokens: .break(.close(mustBreak: false), size: 0)) } before(node.elseKeyword, tokens: .break(.reset), .open) @@ -571,7 +571,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { after( typeAnnotation.colon, tokens: .break(.open(kind: .continuation), newlines: .elective(ignoresDiscretionary: true))) - after(typeAnnotation.lastToken, tokens: .break(.close(mustBreak: false), size: 0)) + after(typeAnnotation.lastToken(viewMode: .sourceAccurate), tokens: .break(.close(mustBreak: false), size: 0)) } arrangeBracesAndContents(of: node.body, contentsKeyPath: \.statements) @@ -591,8 +591,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // condition could be longer than the column limit since there are no breaks between the label // or while token. for condition in node.conditions.dropFirst() { - before(condition.firstToken, tokens: .break(.open(kind: .continuation), size: 0)) - after(condition.lastToken, tokens: .break(.close(mustBreak: false), size: 0)) + before(condition.firstToken(viewMode: .sourceAccurate), tokens: .break(.open(kind: .continuation), size: 0)) + after(condition.lastToken(viewMode: .sourceAccurate), tokens: .break(.close(mustBreak: false), size: 0)) } arrangeBracesAndContents(of: node.body, contentsKeyPath: \.statements) @@ -605,7 +605,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { if config.lineBreakBeforeControlFlowKeywords { before(node.whileKeyword, tokens: .break(.same), .open) - after(node.condition.lastToken, tokens: .close) + after(node.condition.lastToken(viewMode: .sourceAccurate), tokens: .close) } else { // The length of the condition needs to force the breaks around the braces of the repeat // stmt's body, so that there's always a break before the right brace when the while & @@ -613,7 +613,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { before(node.whileKeyword, tokens: .space) // The `open` token occurs after the ending tokens for the braced `body` node. before(node.body.rightBrace, tokens: .open) - after(node.condition.lastToken, tokens: .close) + after(node.condition.lastToken(viewMode: .sourceAccurate), tokens: .close) } after(node.whileKeyword, tokens: .space) return .visitChildren @@ -635,11 +635,11 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // old (pre-SE-0276) behavior (a fixed space after the `catch` keyword). if catchItems.count > 1 { for catchItem in catchItems { - before(catchItem.firstToken, tokens: .break(.open(kind: .continuation))) - after(catchItem.lastToken, tokens: .break(.close(mustBreak: false), size: 0)) + before(catchItem.firstToken(viewMode: .sourceAccurate), tokens: .break(.open(kind: .continuation))) + after(catchItem.lastToken(viewMode: .sourceAccurate), tokens: .break(.close(mustBreak: false), size: 0)) } } else { - before(node.catchItems?.firstToken, tokens: .space) + before(node.catchItems?.firstToken(viewMode: .sourceAccurate), tokens: .space) } } @@ -661,17 +661,17 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: ReturnStmtSyntax) -> SyntaxVisitorContinueKind { if let expression = node.expression { if leftmostMultilineStringLiteral(of: expression) != nil { - before(expression.firstToken, tokens: .break(.open)) - after(expression.lastToken, tokens: .break(.close(mustBreak: false))) + before(expression.firstToken(viewMode: .sourceAccurate), tokens: .break(.open)) + after(expression.lastToken(viewMode: .sourceAccurate), tokens: .break(.close(mustBreak: false))) } else { - before(expression.firstToken, tokens: .break) + before(expression.firstToken(viewMode: .sourceAccurate), tokens: .break) } } return .visitChildren } override func visit(_ node: ThrowStmtSyntax) -> SyntaxVisitorContinueKind { - before(node.expression.firstToken, tokens: .break) + before(node.expression.firstToken(viewMode: .sourceAccurate), tokens: .break) return .visitChildren } @@ -690,10 +690,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // if-configuration clause requires a break here in order to be allowed on a new line. for ifConfigDecl in node.cases.filter({ $0.is(IfConfigDeclSyntax.self) }) { if config.indentSwitchCaseLabels { - before(ifConfigDecl.firstToken, tokens: .break(.open)) - after(ifConfigDecl.lastToken, tokens: .break(.close, size: 0)) + before(ifConfigDecl.firstToken(viewMode: .sourceAccurate), tokens: .break(.open)) + after(ifConfigDecl.lastToken(viewMode: .sourceAccurate), tokens: .break(.close, size: 0)) } else { - before(ifConfigDecl.firstToken, tokens: .break(.same)) + before(ifConfigDecl.firstToken(viewMode: .sourceAccurate), tokens: .break(.same)) } } @@ -713,10 +713,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } else { openBreak = .break(.same, newlines: .soft) } - before(node.firstToken, tokens: openBreak) + before(node.firstToken(viewMode: .sourceAccurate), tokens: openBreak) - after(node.unknownAttr?.lastToken, tokens: .space) - after(node.label.lastToken, tokens: .break(.reset, size: 0), .break(.open), .open) + after(node.unknownAttr?.lastToken(viewMode: .sourceAccurate), tokens: .space) + after(node.label.lastToken(viewMode: .sourceAccurate), tokens: .break(.reset, size: 0), .break(.open), .open) // If switch/case labels were configured to be indented, insert an extra `close` break after // the case body to match the `open` break above @@ -730,10 +730,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // same effect. If instead the opening and closing tokens were omitted completely in the absence // of statements, comments within the empty case would be incorrectly indented to the same level // as the case label. - if node.label.lastToken != node.lastToken { - after(node.lastToken, tokens: afterLastTokenTokens) + if node.label.lastToken(viewMode: .sourceAccurate) != node.lastToken(viewMode: .sourceAccurate) { + after(node.lastToken(viewMode: .sourceAccurate), tokens: afterLastTokenTokens) } else { - before(node.nextToken, tokens: afterLastTokenTokens) + before(node.nextToken(viewMode: .sourceAccurate), tokens: afterLastTokenTokens) } return .visitChildren @@ -749,7 +749,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // following a `NoCasesWithOnlyFallthrough` transformation that might merge cases. let caseItems = Array(node.caseItems) for (index, item) in caseItems.enumerated() { - before(item.firstToken, tokens: .open) + before(item.firstToken(viewMode: .sourceAccurate), tokens: .open) if let trailingComma = item.trailingComma { // Insert a newline before the next item if it has a where clause and this item doesn't. let nextItemHasWhereClause = @@ -758,7 +758,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { let newlines: NewlineBehavior = requiresNewline ? .soft : .elective after(trailingComma, tokens: .close, .break(.continue, size: 1, newlines: newlines)) } else { - after(item.lastToken, tokens: .close) + after(item.lastToken(viewMode: .sourceAccurate), tokens: .close) } } @@ -837,9 +837,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { /// /// - Parameter node: The tuple expression element to be arranged. private func arrangeAsTupleExprElement(_ node: TupleExprElementSyntax) { - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) after(node.colon, tokens: .break) - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) if let trailingComma = node.trailingComma { closingDelimiterTokens.insert(trailingComma) } @@ -857,8 +857,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { insertTokens(.break(.same), betweenElementsOf: node) for element in node { - before(element.firstToken, tokens: .open) - after(element.lastToken, tokens: .close) + before(element.firstToken(viewMode: .sourceAccurate), tokens: .open) + after(element.lastToken(viewMode: .sourceAccurate), tokens: .close) if let trailingComma = element.trailingComma { closingDelimiterTokens.insert(trailingComma) } @@ -868,12 +868,12 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { if let trailingComma = lastElement.trailingComma { ignoredTokens.insert(trailingComma) } - before(node.first?.firstToken, tokens: .commaDelimitedRegionStart) + before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart) let endToken = Token.commaDelimitedRegionEnd( hasTrailingComma: lastElement.trailingComma != nil, isSingleElement: node.first == lastElement) - after(lastElement.expression.lastToken, tokens: [endToken]) + after(lastElement.expression.lastToken(viewMode: .sourceAccurate), tokens: [endToken]) } return .visitChildren } @@ -899,9 +899,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { insertTokens(.break(.same), betweenElementsOf: node) for element in node { - before(element.firstToken, tokens: .open) + before(element.firstToken(viewMode: .sourceAccurate), tokens: .open) after(element.colon, tokens: .break) - after(element.lastToken, tokens: .close) + after(element.lastToken(viewMode: .sourceAccurate), tokens: .close) if let trailingComma = element.trailingComma { closingDelimiterTokens.insert(trailingComma) } @@ -911,12 +911,12 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { if let trailingComma = lastElement.trailingComma { ignoredTokens.insert(trailingComma) } - before(node.first?.firstToken, tokens: .commaDelimitedRegionStart) + before(node.first?.firstToken(viewMode: .sourceAccurate), tokens: .commaDelimitedRegionStart) let endToken = Token.commaDelimitedRegionEnd( hasTrailingComma: lastElement.trailingComma != nil, isSingleElement: node.first == node.last) - after(lastElement.lastToken, tokens: endToken) + after(lastElement.lastToken(viewMode: .sourceAccurate), tokens: endToken) } return .visitChildren } @@ -958,10 +958,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // When this function call is wrapped by a try-expr or await-expr, the group applied when // visiting that wrapping expression is sufficient. Adding another group here in that case // can result in unnecessarily breaking after the try/await keyword. - if !(base.firstToken?.previousToken(viewMode: .all)?.parent?.is(TryExprSyntax.self) ?? false - || base.firstToken?.previousToken(viewMode: .all)?.parent?.is(AwaitExprSyntax.self) ?? false) { - before(base.firstToken, tokens: .open) - after(calledMemberAccessExpr.name.lastToken, tokens: .close) + if !(base.firstToken(viewMode: .sourceAccurate)?.previousToken(viewMode: .all)?.parent?.is(TryExprSyntax.self) ?? false + || base.firstToken(viewMode: .sourceAccurate)?.previousToken(viewMode: .all)?.parent?.is(AwaitExprSyntax.self) ?? false) { + before(base.firstToken(viewMode: .sourceAccurate), tokens: .open) + after(calledMemberAccessExpr.name.lastToken(viewMode: .sourceAccurate), tokens: .close) } } } @@ -1049,7 +1049,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { shouldGroup: Bool ) { if shouldGroup { - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) } var additionalEndTokens = [Token]() @@ -1077,7 +1077,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } after(trailingComma, tokens: afterTrailingComma) } else if shouldGroup { - after(node.lastToken, tokens: additionalEndTokens + [.close]) + after(node.lastToken(viewMode: .sourceAccurate), tokens: additionalEndTokens + [.close]) } } @@ -1118,7 +1118,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: ClosureSignatureSyntax) -> SyntaxVisitorContinueKind { - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) arrangeAttributeList( node.attributes, suppressFinalBreak: node.input == nil && node.capture == nil) @@ -1147,16 +1147,16 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } else { // Group outside of the parens, so that the argument list together, preferring to break // between the argument list and the output. - before(input.firstToken, tokens: .open) - after(input.lastToken, tokens: .close) + before(input.firstToken(viewMode: .sourceAccurate), tokens: .open) + after(input.lastToken(viewMode: .sourceAccurate), tokens: .close) } arrangeClosureParameterClause(parameterClause, forcesBreakBeforeRightParen: true) } else { // Group around the arguments, but don't use open/close breaks because there are no parens // to create a new scope. - before(input.firstToken, tokens: .open(argumentListConsistency())) - after(input.lastToken, tokens: .close) + before(input.firstToken(viewMode: .sourceAccurate), tokens: .open(argumentListConsistency())) + after(input.lastToken(viewMode: .sourceAccurate), tokens: .close) } } @@ -1168,7 +1168,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } before(node.output?.arrow, tokens: .break) - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) before(node.inTok, tokens: .break(.same)) return .visitChildren } @@ -1180,15 +1180,15 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: ClosureCaptureItemSyntax) -> SyntaxVisitorContinueKind { - before(node.firstToken, tokens: .open) - after(node.specifier?.lastToken, tokens: .break) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) + after(node.specifier?.lastToken(viewMode: .sourceAccurate), tokens: .break) before(node.assignToken, tokens: .break) after(node.assignToken, tokens: .break) if let trailingComma = node.trailingComma { before(trailingComma, tokens: .close) after(trailingComma, tokens: .break(.same)) } else { - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) } return .visitChildren } @@ -1198,8 +1198,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { if let calledMemberAccessExpr = node.calledExpression.as(MemberAccessExprSyntax.self) { if let base = calledMemberAccessExpr.base, base.is(IdentifierExprSyntax.self) { - before(base.firstToken, tokens: .open) - after(calledMemberAccessExpr.name.lastToken, tokens: .close) + before(base.firstToken(viewMode: .sourceAccurate), tokens: .open) + after(calledMemberAccessExpr.name.lastToken(viewMode: .sourceAccurate), tokens: .close) } } @@ -1292,7 +1292,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: ClosureParameterSyntax) -> SyntaxVisitorContinueKind { - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) arrangeAttributeList(node.attributes) before( node.secondName, @@ -1302,13 +1302,13 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { if let trailingComma = node.trailingComma { after(trailingComma, tokens: .close, .break(.same)) } else { - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) } return .visitChildren } override func visit(_ node: EnumCaseParameterSyntax) -> SyntaxVisitorContinueKind { - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) before( node.secondName, tokens: .break(.continue, newlines: .elective(ignoresDiscretionary: true))) @@ -1317,13 +1317,13 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { if let trailingComma = node.trailingComma { after(trailingComma, tokens: .close, .break(.same)) } else { - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) } return .visitChildren } override func visit(_ node: FunctionParameterSyntax) -> SyntaxVisitorContinueKind { - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) arrangeAttributeList(node.attributes) before( node.secondName, @@ -1333,7 +1333,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { if let trailingComma = node.trailingComma { after(trailingComma, tokens: .close, .break(.same)) } else { - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) } return .visitChildren } @@ -1345,7 +1345,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // `ReturnClauseSyntax`. To maintain the previous formatting behavior, // add a special case. before(node.arrow, tokens: .break) - before(node.returnType.firstToken, tokens: .break) + before(node.returnType.firstToken(viewMode: .sourceAccurate), tokens: .break) } else { after(node.arrow, tokens: .space) } @@ -1353,8 +1353,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // Member type identifier is used when the return type is a member of another type. Add a group // here so that the base, dot, and member type are kept together when they fit. if node.returnType.is(MemberTypeIdentifierSyntax.self) { - before(node.returnType.firstToken, tokens: .open) - after(node.returnType.lastToken, tokens: .close) + before(node.returnType.firstToken(viewMode: .sourceAccurate), tokens: .open) + after(node.returnType.lastToken(viewMode: .sourceAccurate), tokens: .close) } return .visitChildren } @@ -1383,13 +1383,13 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { breakKindClose = .same } - let tokenToOpenWith = node.condition?.lastToken ?? node.poundKeyword + let tokenToOpenWith = node.condition?.lastToken(viewMode: .sourceAccurate) ?? node.poundKeyword after(tokenToOpenWith, tokens: .break(breakKindOpen), .open) // Unlike other code blocks, where we may want a single statement to be laid out on the same // line as a parent construct, the content of an `#if` block must always be on its own line; // the newline token inserted at the end enforces this. - if let lastElemTok = node.elements?.lastToken { + if let lastElemTok = node.elements?.lastToken(viewMode: .sourceAccurate) { after(lastElemTok, tokens: .break(breakKindClose, newlines: .soft), .close) } else { before(tokenToOpenWith.nextToken(viewMode: .all), tokens: .break(breakKindClose, newlines: .soft), .close) @@ -1400,7 +1400,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { let currentIfConfigDecl = node.parent?.parent?.as(IfConfigDeclSyntax.self) if let currentIfConfigDecl = currentIfConfigDecl, - let tokenBeforeCurrentIfConfigDecl = currentIfConfigDecl.previousToken, + let tokenBeforeCurrentIfConfigDecl = currentIfConfigDecl.previousToken(viewMode: .sourceAccurate), isNestedInIfConfig(node: Syntax(tokenBeforeCurrentIfConfigDecl)) || tokenBeforeCurrentIfConfigDecl.text == "}" { breakToken = .break(.reset) @@ -1410,16 +1410,16 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } before( - node.firstToken, + node.firstToken(viewMode: .sourceAccurate), tokens: [ .printerControl(kind: .enableBreaking), breakToken, ] ) } else if let condition = node.condition { - before(condition.firstToken, tokens: .printerControl(kind: .disableBreaking(allowDiscretionary: true))) + before(condition.firstToken(viewMode: .sourceAccurate), tokens: .printerControl(kind: .disableBreaking(allowDiscretionary: true))) after( - condition.lastToken, + condition.lastToken(viewMode: .sourceAccurate), tokens: .printerControl(kind: .enableBreaking), .break(.reset, size: 0)) } @@ -1434,11 +1434,11 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // Skip ignored items, because the tokens after `item.lastToken` would be ignored and leave // unclosed open tokens. for item in node where !shouldFormatterIgnore(node: Syntax(item)) { - before(item.firstToken, tokens: .open) + before(item.firstToken(viewMode: .sourceAccurate), tokens: .open) let newlines: NewlineBehavior = item != node.last && shouldInsertNewline(basedOn: item.semicolon) ? .soft : .elective let resetSize = item.semicolon != nil ? 1 : 0 - after(item.lastToken, tokens: .close, .break(.reset, size: resetSize, newlines: newlines)) + after(item.lastToken(viewMode: .sourceAccurate), tokens: .close, .break(.reset, size: resetSize, newlines: newlines)) } return .visitChildren } @@ -1461,12 +1461,12 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: EnumCaseDeclSyntax) -> SyntaxVisitorContinueKind { - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) arrangeAttributeList(node.attributes) after(node.caseKeyword, tokens: .break) - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) return .visitChildren } @@ -1478,7 +1478,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: OperatorPrecedenceAndTypesSyntax) -> SyntaxVisitorContinueKind { before(node.colon, tokens: .space) after(node.colon, tokens: .break(.open), .open) - after(node.designatedTypes.lastToken ?? node.lastToken, tokens: .break(.close, size: 0), .close) + after(node.designatedTypes.lastToken(viewMode: .sourceAccurate) ?? node.lastToken(viewMode: .sourceAccurate), tokens: .break(.close, size: 0), .close) return .visitChildren } @@ -1512,13 +1512,13 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: PrecedenceGroupRelationSyntax) -> SyntaxVisitorContinueKind { after(node.colon, tokens: .break(.open)) - after(node.lastToken, tokens: .break(.close, newlines: .soft)) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .break(.close, newlines: .soft)) return .visitChildren } override func visit(_ node: PrecedenceGroupAssignmentSyntax) -> SyntaxVisitorContinueKind { after(node.colon, tokens: .break(.open)) - after(node.lastToken, tokens: .break(.close, newlines: .soft)) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .break(.close, newlines: .soft)) return .visitChildren } @@ -1529,7 +1529,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: PrecedenceGroupAssociativitySyntax) -> SyntaxVisitorContinueKind { after(node.colon, tokens: .break(.open)) - after(node.lastToken, tokens: .break(.close, newlines: .soft)) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .break(.close, newlines: .soft)) return .visitChildren } @@ -1541,11 +1541,11 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // Skip ignored items, because the tokens after `item.lastToken` would be ignored and leave // unclosed open tokens. for item in node where !shouldFormatterIgnore(node: Syntax(item)) { - before(item.firstToken, tokens: .open) + before(item.firstToken(viewMode: .sourceAccurate), tokens: .open) let newlines: NewlineBehavior = item != node.last && shouldInsertNewline(basedOn: item.semicolon) ? .soft : .elective let resetSize = item.semicolon != nil ? 1 : 0 - after(item.lastToken, tokens: .close, .break(.reset, size: resetSize, newlines: newlines)) + after(item.lastToken(viewMode: .sourceAccurate), tokens: .close, .break(.reset, size: resetSize, newlines: newlines)) } return .visitChildren } @@ -1560,8 +1560,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // breaking behavior. if let exprStmt = node.item.as(ExpressionStmtSyntax.self), let ifStmt = exprStmt.expression.as(IfExprSyntax.self) { - before(ifStmt.conditions.firstToken, tokens: .open(.consistent)) - after(ifStmt.lastToken, tokens: .close) + before(ifStmt.conditions.firstToken(viewMode: .sourceAccurate), tokens: .open(.consistent)) + after(ifStmt.lastToken(viewMode: .sourceAccurate), tokens: .close) } return .visitChildren } @@ -1589,7 +1589,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: TupleTypeElementSyntax) -> SyntaxVisitorContinueKind { - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) before( node.secondName, tokens: .break(.continue, newlines: .elective(ignoresDiscretionary: true))) @@ -1598,7 +1598,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { if let trailingComma = node.trailingComma { after(trailingComma, tokens: .close, .break(.same)) } else { - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) } return .visitChildren } @@ -1625,7 +1625,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: TryExprSyntax) -> SyntaxVisitorContinueKind { before( - node.expression.firstToken, + node.expression.firstToken(viewMode: .sourceAccurate), tokens: .break(.continue, newlines: .elective(ignoresDiscretionary: true))) // Check for an anchor token inside of the expression to group with the try keyword. @@ -1639,7 +1639,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: AwaitExprSyntax) -> SyntaxVisitorContinueKind { before( - node.expression.firstToken, + node.expression.firstToken(viewMode: .sourceAccurate), tokens: .break(.continue, newlines: .elective(ignoresDiscretionary: true))) // Check for an anchor token inside of the expression to group with the await keyword. @@ -1672,12 +1672,12 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // sequence. This check has to happen here so that the `MemberAccessExprSyntax.name` is // available. if base.is(IdentifierExprSyntax.self) { - return memberAccessExpr.name.lastToken + return memberAccessExpr.name.lastToken(viewMode: .sourceAccurate) } return findTryAwaitExprConnectingToken(inExpr: base) } if expr.is(IdentifierExprSyntax.self) { - return expr.lastToken + return expr.lastToken(viewMode: .sourceAccurate) } return nil } @@ -1687,7 +1687,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: AttributeSyntax) -> SyntaxVisitorContinueKind { - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) switch node.argument { case .argumentList(let argumentList)?: if let leftParen = node.leftParen, let rightParen = node.rightParen { @@ -1707,7 +1707,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { case nil: break } - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) return .visitChildren } @@ -1719,24 +1719,24 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: AvailabilityLabeledArgumentSyntax) -> SyntaxVisitorContinueKind { before(node.label, tokens: .open) after(node.colon, tokens: .break(.continue, newlines: .elective(ignoresDiscretionary: true))) - after(node.value.lastToken, tokens: .close) + after(node.value.lastToken(viewMode: .sourceAccurate), tokens: .close) return .visitChildren } override func visit(_ node: AvailabilityVersionRestrictionSyntax) -> SyntaxVisitorContinueKind { - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) after(node.platform, tokens: .break(.continue, size: 1)) - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) return .visitChildren } override func visit(_ node: ConditionElementSyntax) -> SyntaxVisitorContinueKind { - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) if let comma = node.trailingComma { after(comma, tokens: .close, .break(.same)) closingDelimiterTokens.insert(comma) } else { - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) } return .visitChildren } @@ -1747,13 +1747,13 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind { // Import declarations should never be wrapped. - before(node.firstToken, tokens: .printerControl(kind: .disableBreaking(allowDiscretionary: false))) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .printerControl(kind: .disableBreaking(allowDiscretionary: false))) arrangeAttributeList(node.attributes) after(node.importTok, tokens: .space) after(node.importKind, tokens: .space) - after(node.lastToken, tokens: .printerControl(kind: .enableBreaking)) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .printerControl(kind: .enableBreaking)) return .visitChildren } @@ -1777,9 +1777,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // that it is glued to the last token of the ternary. let closeScopeToken: TokenSyntax? if let parenExpr = outermostEnclosingNode(from: Syntax(node.secondChoice)) { - closeScopeToken = parenExpr.lastToken + closeScopeToken = parenExpr.lastToken(viewMode: .sourceAccurate) } else { - closeScopeToken = node.secondChoice.lastToken + closeScopeToken = node.secondChoice.lastToken(viewMode: .sourceAccurate) } after(closeScopeToken, tokens: .break(.close(mustBreak: false), size: 0), .close, .close) return .visitChildren @@ -1813,7 +1813,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } before(node.whereKeyword, tokens: wherePrecedingBreak, .open) after(node.whereKeyword, tokens: .break) - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) return .visitChildren } @@ -1827,7 +1827,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } else { breakOrSpace = .break } - after(node.lastToken, tokens: breakOrSpace) + after(node.lastToken(viewMode: .sourceAccurate), tokens: breakOrSpace) return .visitChildren } @@ -1840,7 +1840,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { before(asyncOrReasyncKeyword, tokens: .open) after(throwsOrRethrowsKeyword, tokens: .close) } - before(node.output?.firstToken, tokens: .break) + before(node.output?.firstToken(viewMode: .sourceAccurate), tokens: .break) return .visitChildren } @@ -1864,7 +1864,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // If there were spaces in the trailing trivia of the previous token, they would have been // ignored (since this expression would be expected to insert its own preceding breaks). // Preserve that whitespace verbatim for now. - if let previousToken = node.firstToken?.previousToken { + if let previousToken = node.firstToken(viewMode: .sourceAccurate)?.previousToken(viewMode: .sourceAccurate) { appendTrailingTrivia(previousToken, forced: true) } verbatimToken(Syntax(node), indentingBehavior: .none) @@ -1886,7 +1886,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { if let (unindentingNode, _, breakKind) = stackedIndentationBehavior(after: binOp, rhs: rhs) { beforeTokens = [.break(.open(kind: breakKind))] - after(unindentingNode.lastToken, tokens: [.break(.close(mustBreak: false), size: 0)]) + after(unindentingNode.lastToken(viewMode: .sourceAccurate), tokens: [.break(.close(mustBreak: false), size: 0)]) } else { beforeTokens = [.break(.continue)] } @@ -1896,10 +1896,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // operator token. if isCompoundExpression(rhs) { beforeTokens.append(.open) - after(rhs.lastToken, tokens: .close) + after(rhs.lastToken(viewMode: .sourceAccurate), tokens: .close) } - after(binOp.lastToken, tokens: beforeTokens) + after(binOp.lastToken(viewMode: .sourceAccurate), tokens: beforeTokens) } else if let (unindentingNode, shouldReset, breakKind) = stackedIndentationBehavior(after: binOp, rhs: rhs) { @@ -1912,27 +1912,27 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // usual single-level "continuation line or not" behavior. let openBreakTokens: [Token] = [.break(.open(kind: breakKind)), .open] if wrapsBeforeOperator { - before(binOp.firstToken, tokens: openBreakTokens) + before(binOp.firstToken(viewMode: .sourceAccurate), tokens: openBreakTokens) } else { - after(binOp.lastToken, tokens: openBreakTokens) + after(binOp.lastToken(viewMode: .sourceAccurate), tokens: openBreakTokens) } let closeBreakTokens: [Token] = (shouldReset ? [.break(.reset, size: 0)] : []) + [.break(.close(mustBreak: false), size: 0), .close] - after(unindentingNode.lastToken, tokens: closeBreakTokens) + after(unindentingNode.lastToken(viewMode: .sourceAccurate), tokens: closeBreakTokens) } else { if wrapsBeforeOperator { - before(binOp.firstToken, tokens: .break(.continue)) + before(binOp.firstToken(viewMode: .sourceAccurate), tokens: .break(.continue)) } else { - after(binOp.lastToken, tokens: .break(.continue)) + after(binOp.lastToken(viewMode: .sourceAccurate), tokens: .break(.continue)) } } if wrapsBeforeOperator { - after(binOp.lastToken, tokens: .space) + after(binOp.lastToken(viewMode: .sourceAccurate), tokens: .space) } else { - before(binOp.firstToken, tokens: .space) + before(binOp.firstToken(viewMode: .sourceAccurate), tokens: .space) } } @@ -1949,15 +1949,15 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: AsExprSyntax) -> SyntaxVisitorContinueKind { before(node.asTok, tokens: .break(.continue), .open) - before(node.typeName.firstToken, tokens: .space) - after(node.lastToken, tokens: .close) + before(node.typeName.firstToken(viewMode: .sourceAccurate), tokens: .space) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) return .visitChildren } override func visit(_ node: IsExprSyntax) -> SyntaxVisitorContinueKind { before(node.isTok, tokens: .break(.continue), .open) - before(node.typeName.firstToken, tokens: .space) - after(node.lastToken, tokens: .close) + before(node.typeName.firstToken(viewMode: .sourceAccurate), tokens: .space) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) return .visitChildren } @@ -2002,12 +2002,12 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { after(node.bindingKeyword, tokens: .break(.open)) for binding in node.bindings { - before(binding.firstToken, tokens: .open) + before(binding.firstToken(viewMode: .sourceAccurate), tokens: .open) after(binding.trailingComma, tokens: .break(.same)) - after(binding.lastToken, tokens: .close) + after(binding.lastToken(viewMode: .sourceAccurate), tokens: .close) } - after(node.lastToken, tokens: .break(.close, size: 0)) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .break(.close, size: 0)) } return .visitChildren @@ -2026,25 +2026,25 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { typeAnnotation.colon, tokens: .break(.open(kind: .continuation), newlines: .elective(ignoresDiscretionary: true))) closesNeeded += 1 - closeAfterToken = typeAnnotation.lastToken + closeAfterToken = typeAnnotation.lastToken(viewMode: .sourceAccurate) } if let initializer = node.initializer { let expr = initializer.value if let (unindentingNode, _, breakKind) = stackedIndentationBehavior(rhs: expr) { after(initializer.equal, tokens: .break(.open(kind: breakKind))) - after(unindentingNode.lastToken, tokens: .break(.close(mustBreak: false), size: 0)) + after(unindentingNode.lastToken(viewMode: .sourceAccurate), tokens: .break(.close(mustBreak: false), size: 0)) } else { after(initializer.equal, tokens: .break(.continue)) } - closeAfterToken = initializer.lastToken + closeAfterToken = initializer.lastToken(viewMode: .sourceAccurate) // When the RHS is a simple expression, even if is requires multiple lines, we don't add a // group so that as much of the expression as possible can stay on the same line as the // operator token. if isCompoundExpression(expr) { - before(expr.firstToken, tokens: .open) - after(expr.lastToken, tokens: .close) + before(expr.firstToken(viewMode: .sourceAccurate), tokens: .open) + after(expr.lastToken(viewMode: .sourceAccurate), tokens: .close) } } @@ -2086,8 +2086,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { after(node.typealiasKeyword, tokens: .break) if let genericWhereClause = node.genericWhereClause { - before(genericWhereClause.firstToken, tokens: .break(.same), .open) - after(node.lastToken, tokens: .close) + before(genericWhereClause.firstToken(viewMode: .sourceAccurate), tokens: .break(.same), .open) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) } return .visitChildren } @@ -2119,8 +2119,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: TypeAnnotationSyntax) -> SyntaxVisitorContinueKind { - before(node.type.firstToken, tokens: .open) - after(node.type.lastToken, tokens: .close) + before(node.type.firstToken(viewMode: .sourceAccurate), tokens: .open) + after(node.type.lastToken(viewMode: .sourceAccurate), tokens: .close) return .visitChildren } @@ -2142,11 +2142,11 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: GenericArgumentSyntax) -> SyntaxVisitorContinueKind { - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) if let trailingComma = node.trailingComma { after(trailingComma, tokens: .close, .break(.same)) } else { - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) } return .visitChildren } @@ -2164,22 +2164,22 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: GenericParameterSyntax) -> SyntaxVisitorContinueKind { - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) after(node.colon, tokens: .break) if let trailingComma = node.trailingComma { after(trailingComma, tokens: .close, .break(.same)) } else { - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) } return .visitChildren } override func visit(_ node: PrimaryAssociatedTypeSyntax) -> SyntaxVisitorContinueKind { - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) if let trailingComma = node.trailingComma { after(trailingComma, tokens: .close, .break(.same)) } else { - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) } return .visitChildren } @@ -2260,8 +2260,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { after(node.associatedtypeKeyword, tokens: .break) if let genericWhereClause = node.genericWhereClause { - before(genericWhereClause.firstToken, tokens: .break(.same), .open) - after(node.lastToken, tokens: .close) + before(genericWhereClause.firstToken(viewMode: .sourceAccurate), tokens: .break(.same), .open) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) } return .visitChildren } @@ -2271,16 +2271,16 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: GenericWhereClauseSyntax) -> SyntaxVisitorContinueKind { - guard node.whereKeyword != node.lastToken else { + guard node.whereKeyword != node.lastToken(viewMode: .sourceAccurate) else { verbatimToken(Syntax(node)) return .skipChildren } after(node.whereKeyword, tokens: .break(.open)) - after(node.lastToken, tokens: .break(.close, size: 0)) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .break(.close, size: 0)) - before(node.requirementList.firstToken, tokens: .open(genericRequirementListConsistency())) - after(node.requirementList.lastToken, tokens: .close) + before(node.requirementList.firstToken(viewMode: .sourceAccurate), tokens: .open(genericRequirementListConsistency())) + after(node.requirementList.lastToken(viewMode: .sourceAccurate), tokens: .close) return .visitChildren } @@ -2294,11 +2294,11 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: GenericRequirementSyntax) -> SyntaxVisitorContinueKind { - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) if let trailingComma = node.trailingComma { after(trailingComma, tokens: .close, .break(.same)) } else { - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) } return .visitChildren } @@ -2350,8 +2350,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // Normally, the open-break is placed before the open token. In this case, it's intentionally // ordered differently so that the inheritance list can start on the current line and only // breaks if the first item in the list would overflow the column limit. - before(node.inheritedTypeCollection.firstToken, tokens: .open, .break(.open, size: 1)) - after(node.inheritedTypeCollection.lastToken, tokens: .break(.close, size: 0), .close) + before(node.inheritedTypeCollection.firstToken(viewMode: .sourceAccurate), tokens: .open, .break(.open, size: 1)) + after(node.inheritedTypeCollection.lastToken(viewMode: .sourceAccurate), tokens: .break(.close, size: 0), .close) return .visitChildren } @@ -2366,9 +2366,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: MatchingPatternConditionSyntax) -> SyntaxVisitorContinueKind { - before(node.firstToken, tokens: .open) + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) after(node.caseKeyword, tokens: .break) - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) return .visitChildren } @@ -2379,7 +2379,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { after( typeAnnotation.colon, tokens: .break(.open(kind: .continuation), newlines: .elective(ignoresDiscretionary: true))) - after(typeAnnotation.lastToken, tokens: .break(.close(mustBreak: false), size: 0)) + after(typeAnnotation.lastToken(viewMode: .sourceAccurate), tokens: .break(.close(mustBreak: false), size: 0)) } return .visitChildren @@ -2403,10 +2403,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { if let whereClause = node.whereClause { if needsBreakBeforeWhereClause { - before(whereClause.firstToken, tokens: .break(.same)) + before(whereClause.firstToken(viewMode: .sourceAccurate), tokens: .break(.same)) } - before(whereClause.firstToken, tokens: .open) - after(whereClause.lastToken, tokens: .close) + before(whereClause.firstToken(viewMode: .sourceAccurate), tokens: .open) + after(whereClause.lastToken(viewMode: .sourceAccurate), tokens: .close) } return .visitChildren } @@ -2588,13 +2588,13 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { suppressFinalBreak: Bool = false ) { if let attributes = attributes { - before(attributes.firstToken, tokens: .open) + before(attributes.firstToken(viewMode: .sourceAccurate), tokens: .open) insertTokens(.break(.same), betweenElementsOf: attributes) var afterAttributeTokens = [Token.close] if !suppressFinalBreak { afterAttributeTokens.append(.break(.same)) } - after(attributes.lastToken, tokens: afterAttributeTokens) + after(attributes.lastToken(viewMode: .sourceAccurate), tokens: afterAttributeTokens) } } @@ -2860,7 +2860,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { private func afterTokensForTrailingComment(_ token: TokenSyntax) -> (isLineComment: Bool, tokens: [Token]) { - let nextToken = token.nextToken + let nextToken = token.nextToken(viewMode: .sourceAccurate) guard let trivia = nextToken?.leadingTrivia, let firstPiece = trivia.first else { @@ -3146,7 +3146,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { /// Returns true if the first token of the given node is an open delimiter that may desire /// special breaking behavior in some cases. private func startsWithOpenDelimiter(_ node: Syntax) -> Bool { - guard let token = node.firstToken else { return false } + guard let token = node.firstToken(viewMode: .sourceAccurate) else { return false } switch token.tokenKind { case .leftBrace, .leftParen, .leftSquareBracket: return true default: return false @@ -3222,8 +3222,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { ) { switch Syntax(expr).as(SyntaxEnum.self) { case .memberAccessExpr, .subscriptExpr: - before(expr.firstToken, tokens: .open) - after(expr.lastToken, tokens: .close) + before(expr.firstToken(viewMode: .sourceAccurate), tokens: .open) + after(expr.lastToken(viewMode: .sourceAccurate), tokens: .close) default: break } @@ -3235,8 +3235,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { if expr.is(FunctionCallExprSyntax.self), let operatorExpr = operatorExpr, !isAssigningOperator(operatorExpr) { - before(expr.firstToken, tokens: .open) - after(expr.lastToken, tokens: .close) + before(expr.firstToken(viewMode: .sourceAccurate), tokens: .open) + after(expr.lastToken(viewMode: .sourceAccurate), tokens: .close) } } @@ -3321,12 +3321,12 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { /// alongside the last token of the given node. Any tokens between `node.lastToken` and the /// returned node's `lastToken` are delimiter tokens that shouldn't be preceded by a break. private func outermostEnclosingNode(from node: Syntax) -> Syntax? { - guard let afterToken = node.lastToken?.nextToken(viewMode: .all), closingDelimiterTokens.contains(afterToken) + guard let afterToken = node.lastToken(viewMode: .sourceAccurate)?.nextToken(viewMode: .all), closingDelimiterTokens.contains(afterToken) else { return nil } var parenthesizedExpr = afterToken.parent - while let nextToken = parenthesizedExpr?.lastToken?.nextToken(viewMode: .all), + while let nextToken = parenthesizedExpr?.lastToken(viewMode: .sourceAccurate)?.nextToken(viewMode: .all), closingDelimiterTokens.contains(nextToken), let nextExpr = nextToken.parent { @@ -3463,7 +3463,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // `verbatim` token in order for the first token to be printed with correct indentation. All // following lines in the ignored node are printed as-is with no changes to indentation. var nodeText = node.description - if let firstToken = node.firstToken { + if let firstToken = node.firstToken(viewMode: .sourceAccurate) { extractLeadingTrivia(firstToken) let leadingTriviaText = firstToken.leadingTrivia.reduce(into: "") { $1.write(to: &$0) } nodeText = String(nodeText.dropFirst(leadingTriviaText.count)) @@ -3472,7 +3472,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // The leading trivia of the next token, after the ignored node, may contain content that // belongs with the ignored node. The trivia extraction that is performed for `lastToken` later // excludes that content so it needs to be extracted and added to the token stream here. - if let next = node.lastToken?.nextToken(viewMode: .all), let trivia = next.leadingTrivia.first { + if let next = node.lastToken(viewMode: .sourceAccurate)?.nextToken(viewMode: .all), let trivia = next.leadingTrivia.first { switch trivia { case .lineComment, .blockComment: trivia.write(to: &nodeText) @@ -3531,8 +3531,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { (hasCompoundExpression, _) = insertContextualBreaks(base, isTopLevel: false) } if isTopLevel { - before(expr.firstToken, tokens: .contextualBreakingStart) - after(expr.lastToken, tokens: .contextualBreakingEnd) + before(expr.firstToken(viewMode: .sourceAccurate), tokens: .contextualBreakingStart) + after(expr.lastToken(viewMode: .sourceAccurate), tokens: .contextualBreakingEnd) } return (hasCompoundExpression, true) } else if let callingExpr = expr.asProtocol(CallingExprSyntaxProtocol.self) { @@ -3557,14 +3557,14 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } } before(calledMemberAccessExpr.dot, tokens: beforeTokens) - after(expr.lastToken, tokens: afterTokens) + after(expr.lastToken(viewMode: .sourceAccurate), tokens: afterTokens) if isTopLevel { - before(expr.firstToken, tokens: .contextualBreakingStart) - after(expr.lastToken, tokens: .contextualBreakingEnd) + before(expr.firstToken(viewMode: .sourceAccurate), tokens: .contextualBreakingStart) + after(expr.lastToken(viewMode: .sourceAccurate), tokens: .contextualBreakingEnd) } } else { - before(expr.firstToken, tokens: beforeTokens) - after(expr.lastToken, tokens: afterTokens) + before(expr.firstToken(viewMode: .sourceAccurate), tokens: beforeTokens) + after(expr.lastToken(viewMode: .sourceAccurate), tokens: afterTokens) } return (true, hasMemberAccess) } @@ -3572,8 +3572,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // Otherwise, it's an expression that isn't calling another expression (e.g. array or // dictionary, identifier, etc.). Wrap it in a breaking context but don't try to pre-visit // children nodes. - before(expr.firstToken, tokens: .contextualBreakingStart) - after(expr.lastToken, tokens: .contextualBreakingEnd) + before(expr.firstToken(viewMode: .sourceAccurate), tokens: .contextualBreakingStart) + after(expr.lastToken(viewMode: .sourceAccurate), tokens: .contextualBreakingEnd) let hasCompoundExpression = !expr.is(IdentifierExprSyntax.self) return (hasCompoundExpression, false) } @@ -3767,7 +3767,7 @@ fileprivate func isFormatterIgnorePresent(inTrivia trivia: Trivia, isWholeFile: fileprivate func shouldFormatterIgnore(node: Syntax) -> Bool { // Regardless of the level of nesting, if the ignore directive is present on the first token // contained within the node then the entire node is eligible for ignoring. - if let firstTrivia = node.firstToken?.leadingTrivia { + if let firstTrivia = node.firstToken(viewMode: .sourceAccurate)?.leadingTrivia { return isFormatterIgnorePresent(inTrivia: firstTrivia, isWholeFile: false) } return false @@ -3779,7 +3779,7 @@ fileprivate func shouldFormatterIgnore(node: Syntax) -> Bool { /// /// - Parameter file: The root syntax node for a source file. fileprivate func shouldFormatterIgnore(file: SourceFileSyntax) -> Bool { - if let firstTrivia = file.firstToken?.leadingTrivia { + if let firstTrivia = file.firstToken(viewMode: .sourceAccurate)?.leadingTrivia { return isFormatterIgnorePresent(inTrivia: firstTrivia, isWholeFile: true) } return false diff --git a/Sources/SwiftFormatRules/AddModifierRewriter.swift b/Sources/SwiftFormatRules/AddModifierRewriter.swift index 4f10b4c85..f58a4472e 100644 --- a/Sources/SwiftFormatRules/AddModifierRewriter.swift +++ b/Sources/SwiftFormatRules/AddModifierRewriter.swift @@ -166,18 +166,18 @@ fileprivate final class AddModifierRewriter: SyntaxRewriter { for modifiersProvider: (NodeType) -> ModifierListSyntax? ) -> NodeType { guard let modifier = modifiersProvider(node)?.firstAndOnly, - let movingLeadingTrivia = modifier.nextToken?.leadingTrivia + let movingLeadingTrivia = modifier.nextToken(viewMode: .sourceAccurate)?.leadingTrivia else { // Otherwise, there's no trivia that needs to be relocated so the node is fine. return node } let nodeWithTrivia = replaceTrivia( on: node, - token: modifier.firstToken, + token: modifier.firstToken(viewMode: .sourceAccurate), leadingTrivia: movingLeadingTrivia) return replaceTrivia( on: nodeWithTrivia, - token: modifiersProvider(nodeWithTrivia)?.first?.nextToken, + token: modifiersProvider(nodeWithTrivia)?.first?.nextToken(viewMode: .sourceAccurate), leadingTrivia: []) } } diff --git a/Sources/SwiftFormatRules/DeclSyntaxProtocol+Comments.swift b/Sources/SwiftFormatRules/DeclSyntaxProtocol+Comments.swift index a496c92c8..b695ed7fe 100644 --- a/Sources/SwiftFormatRules/DeclSyntaxProtocol+Comments.swift +++ b/Sources/SwiftFormatRules/DeclSyntaxProtocol+Comments.swift @@ -15,7 +15,7 @@ import SwiftSyntax extension DeclSyntaxProtocol { /// Searches through the leading trivia of this decl for a documentation comment. var docComment: String? { - guard let tok = firstToken else { return nil } + guard let tok = firstToken(viewMode: .sourceAccurate) else { return nil } var comment = [String]() // We need to skip trivia until we see the first comment. This trivia will include all the diff --git a/Sources/SwiftFormatRules/DoNotUseSemicolons.swift b/Sources/SwiftFormatRules/DoNotUseSemicolons.swift index 0aa080a3b..604ece259 100644 --- a/Sources/SwiftFormatRules/DoNotUseSemicolons.swift +++ b/Sources/SwiftFormatRules/DoNotUseSemicolons.swift @@ -56,7 +56,7 @@ public final class DoNotUseSemicolons: SyntaxFormatRule { defer { newItems[idx] = newItem } // Check if the leading trivia for this statement needs a new line. - if previousHadSemicolon, let firstToken = newItem.firstToken, + if previousHadSemicolon, let firstToken = newItem.firstToken(viewMode: .sourceAccurate), !firstToken.leadingTrivia.containsNewlines { let leadingTrivia = .newlines(1) + firstToken.leadingTrivia diff --git a/Sources/SwiftFormatRules/FullyIndirectEnum.swift b/Sources/SwiftFormatRules/FullyIndirectEnum.swift index d4b20652d..2853f55ec 100644 --- a/Sources/SwiftFormatRules/FullyIndirectEnum.swift +++ b/Sources/SwiftFormatRules/FullyIndirectEnum.swift @@ -53,14 +53,14 @@ public final class FullyIndirectEnum: SyntaxFormatRule { // If the `indirect` keyword being added would be the first token in the decl, we need to move // the leading trivia from the `enum` keyword to the new modifier to preserve the existing // line breaks/comments/indentation. - let firstTok = node.firstToken! + let firstTok = node.firstToken(viewMode: .sourceAccurate)! let leadingTrivia: Trivia let newEnumDecl: EnumDeclSyntax if firstTok.tokenKind == .keyword(.enum) { leadingTrivia = firstTok.leadingTrivia newEnumDecl = replaceTrivia( - on: node, token: node.firstToken, leadingTrivia: []) + on: node, token: node.firstToken(viewMode: .sourceAccurate), leadingTrivia: []) } else { leadingTrivia = [] newEnumDecl = node @@ -97,7 +97,7 @@ public final class FullyIndirectEnum: SyntaxFormatRule { ) -> EnumCaseDeclSyntax { if let modifiers = unformattedCase.modifiers, let first = modifiers.first { return replaceTrivia( - on: unformattedCase, token: first.firstToken, leadingTrivia: leadingTrivia + on: unformattedCase, token: first.firstToken(viewMode: .sourceAccurate), leadingTrivia: leadingTrivia ) } else { return replaceTrivia( diff --git a/Sources/SwiftFormatRules/ModifierListSyntax+Convenience.swift b/Sources/SwiftFormatRules/ModifierListSyntax+Convenience.swift index eb7cebab9..7a4b82564 100644 --- a/Sources/SwiftFormatRules/ModifierListSyntax+Convenience.swift +++ b/Sources/SwiftFormatRules/ModifierListSyntax+Convenience.swift @@ -72,12 +72,12 @@ extension ModifierListSyntax { if index == 0 { guard formatTrivia else { return inserting(modifier, at: index) } - guard let firstMod = first, let firstTok = firstMod.firstToken else { + guard let firstMod = first, let firstTok = firstMod.firstToken(viewMode: .sourceAccurate) else { return inserting(modifier, at: index) } let formattedMod = replaceTrivia( on: modifier, - token: modifier.firstToken, + token: modifier.firstToken(viewMode: .sourceAccurate), leadingTrivia: firstTok.leadingTrivia) newModifiers[0] = replaceTrivia( on: firstMod, diff --git a/Sources/SwiftFormatRules/NoCasesWithOnlyFallthrough.swift b/Sources/SwiftFormatRules/NoCasesWithOnlyFallthrough.swift index cb5f6dc01..f8966ec2c 100644 --- a/Sources/SwiftFormatRules/NoCasesWithOnlyFallthrough.swift +++ b/Sources/SwiftFormatRules/NoCasesWithOnlyFallthrough.swift @@ -150,7 +150,7 @@ public final class NoCasesWithOnlyFallthrough: SyntaxFormatRule { // Check for any comments that are inline on the fallthrough statement. Inline comments are // always stored in the next token's leading trivia. - if let nextLeadingTrivia = onlyStatement.nextToken?.leadingTrivia, + if let nextLeadingTrivia = onlyStatement.nextToken(viewMode: .sourceAccurate)?.leadingTrivia, nextLeadingTrivia.prefix(while: { !$0.isNewline }).contains(where: { $0.isComment }) { return false diff --git a/Sources/SwiftFormatRules/NoEmptyTrailingClosureParentheses.swift b/Sources/SwiftFormatRules/NoEmptyTrailingClosureParentheses.swift index 2df45668d..6bc786e98 100644 --- a/Sources/SwiftFormatRules/NoEmptyTrailingClosureParentheses.swift +++ b/Sources/SwiftFormatRules/NoEmptyTrailingClosureParentheses.swift @@ -29,7 +29,7 @@ public final class NoEmptyTrailingClosureParentheses: SyntaxFormatRule { { return super.visit(node) } - guard let name = node.calledExpression.lastToken?.with(\.leadingTrivia, []).with(\.trailingTrivia, []) else { + guard let name = node.calledExpression.lastToken(viewMode: .sourceAccurate)?.with(\.leadingTrivia, []).with(\.trailingTrivia, []) else { return super.visit(node) } @@ -42,7 +42,7 @@ public final class NoEmptyTrailingClosureParentheses: SyntaxFormatRule { } let formattedExp = replaceTrivia( on: rewrittenCalledExpr, - token: rewrittenCalledExpr.lastToken, + token: rewrittenCalledExpr.lastToken(viewMode: .sourceAccurate), trailingTrivia: .spaces(1)) let formattedClosure = visit(trailingClosure).as(ClosureExprSyntax.self) let result = node.with(\.leftParen, nil).with(\.rightParen, nil).with(\.calledExpression, formattedExp) diff --git a/Sources/SwiftFormatRules/NoParensAroundConditions.swift b/Sources/SwiftFormatRules/NoParensAroundConditions.swift index 1ee8678a7..b860b4590 100644 --- a/Sources/SwiftFormatRules/NoParensAroundConditions.swift +++ b/Sources/SwiftFormatRules/NoParensAroundConditions.swift @@ -52,7 +52,7 @@ public final class NoParensAroundConditions: SyntaxFormatRule { } return replaceTrivia( on: visitedExpr, - token: visitedExpr.lastToken, + token: visitedExpr.lastToken(viewMode: .sourceAccurate), leadingTrivia: visitedTuple.leftParen.leadingTrivia, trailingTrivia: visitedTuple.rightParen.trailingTrivia ) diff --git a/Sources/SwiftFormatRules/OneVariableDeclarationPerLine.swift b/Sources/SwiftFormatRules/OneVariableDeclarationPerLine.swift index b21207b74..ff35a1ed1 100644 --- a/Sources/SwiftFormatRules/OneVariableDeclarationPerLine.swift +++ b/Sources/SwiftFormatRules/OneVariableDeclarationPerLine.swift @@ -163,7 +163,7 @@ private struct VariableDeclSplitter { // lines because the pretty printer will re-indent them correctly; we just // need to ensure that a newline is inserted before new decls. varDecl = replaceTrivia( - on: varDecl, token: varDecl.firstToken, leadingTrivia: .newlines(1)) + on: varDecl, token: varDecl.firstToken(viewMode: .sourceAccurate), leadingTrivia: .newlines(1)) fixedUpTrivia = true } diff --git a/Sources/SwiftFormatRules/OrderedImports.swift b/Sources/SwiftFormatRules/OrderedImports.swift index 187b79673..9b3e9730a 100644 --- a/Sources/SwiftFormatRules/OrderedImports.swift +++ b/Sources/SwiftFormatRules/OrderedImports.swift @@ -362,7 +362,7 @@ fileprivate func convertToCodeBlockItems(lines: [Line]) -> [CodeBlockItemSyntax] output.append( replaceTrivia( on: codeBlockItem, - token: codeBlockItem.firstToken, + token: codeBlockItem.firstToken(viewMode: .sourceAccurate), leadingTrivia: Trivia(pieces: triviaBuffer) ) ) @@ -509,17 +509,17 @@ fileprivate class Line { guard let syntaxNode = syntaxNode else { return nil } switch syntaxNode { case .importCodeBlock(let codeBlock, _): - return codeBlock.firstToken + return codeBlock.firstToken(viewMode: .sourceAccurate) case .nonImportCodeBlocks(let codeBlocks): - return codeBlocks.first?.firstToken + return codeBlocks.first?.firstToken(viewMode: .sourceAccurate) } } /// Returns a `LineType` the represents the type of import from the given import decl. private func importType(of importDecl: ImportDeclSyntax) -> LineType { - if let attr = importDecl.attributes?.firstToken, + if let attr = importDecl.attributes?.firstToken(viewMode: .sourceAccurate), attr.tokenKind == .atSign, - attr.nextToken?.text == "testable" + attr.nextToken(viewMode: .sourceAccurate)?.text == "testable" { return .testableImport } diff --git a/Sources/SwiftFormatRules/ReturnVoidInsteadOfEmptyTuple.swift b/Sources/SwiftFormatRules/ReturnVoidInsteadOfEmptyTuple.swift index b1e29c06e..baa0a8223 100644 --- a/Sources/SwiftFormatRules/ReturnVoidInsteadOfEmptyTuple.swift +++ b/Sources/SwiftFormatRules/ReturnVoidInsteadOfEmptyTuple.swift @@ -105,8 +105,8 @@ public final class ReturnVoidInsteadOfEmptyTuple: SyntaxFormatRule { return SimpleTypeIdentifierSyntax( name: TokenSyntax.identifier( "Void", - leadingTrivia: node.firstToken?.leadingTrivia ?? [], - trailingTrivia: node.lastToken?.trailingTrivia ?? []), + leadingTrivia: node.firstToken(viewMode: .sourceAccurate)?.leadingTrivia ?? [], + trailingTrivia: node.lastToken(viewMode: .sourceAccurate)?.trailingTrivia ?? []), genericArgumentClause: nil) } } diff --git a/Sources/SwiftFormatRules/UseShorthandTypeNames.swift b/Sources/SwiftFormatRules/UseShorthandTypeNames.swift index 8def63c71..e1844b898 100644 --- a/Sources/SwiftFormatRules/UseShorthandTypeNames.swift +++ b/Sources/SwiftFormatRules/UseShorthandTypeNames.swift @@ -258,7 +258,7 @@ public final class UseShorthandTypeNames: SyntaxFormatRule { // instead of discarding the comment. wrappedType = replaceTrivia( - on: wrappedType, token: wrappedType.firstToken, leadingTrivia: leadingTrivia) + on: wrappedType, token: wrappedType.firstToken(viewMode: .sourceAccurate), leadingTrivia: leadingTrivia) } let optionalType = OptionalTypeSyntax( @@ -346,7 +346,7 @@ public final class UseShorthandTypeNames: SyntaxFormatRule { // the comment. wrappedTypeExpr = replaceTrivia( - on: wrappedTypeExpr, token: wrappedTypeExpr.firstToken, leadingTrivia: leadingTrivia) + on: wrappedTypeExpr, token: wrappedTypeExpr.firstToken(viewMode: .sourceAccurate), leadingTrivia: leadingTrivia) } return OptionalChainingExprSyntax( @@ -410,7 +410,7 @@ public final class UseShorthandTypeNames: SyntaxFormatRule { case .optionalType(let optionalType): let result = makeOptionalTypeExpression( wrapping: optionalType.wrappedType, - leadingTrivia: optionalType.firstToken?.leadingTrivia, + leadingTrivia: optionalType.firstToken(viewMode: .sourceAccurate)?.leadingTrivia, questionMark: optionalType.questionMark) return ExprSyntax(result) @@ -493,8 +493,8 @@ public final class UseShorthandTypeNames: SyntaxFormatRule { -> (leadingTrivia: Trivia, trailingTrivia: Trivia) { return ( - leadingTrivia: node.firstToken?.leadingTrivia ?? [], - trailingTrivia: node.lastToken?.trailingTrivia ?? [] + leadingTrivia: node.firstToken(viewMode: .sourceAccurate)?.leadingTrivia ?? [], + trailingTrivia: node.lastToken(viewMode: .sourceAccurate)?.trailingTrivia ?? [] ) } diff --git a/Sources/SwiftFormatRules/UseTripleSlashForDocumentationComments.swift b/Sources/SwiftFormatRules/UseTripleSlashForDocumentationComments.swift index d2c981433..4a976541b 100644 --- a/Sources/SwiftFormatRules/UseTripleSlashForDocumentationComments.swift +++ b/Sources/SwiftFormatRules/UseTripleSlashForDocumentationComments.swift @@ -98,7 +98,7 @@ public final class UseTripleSlashForDocumentationComments: SyntaxFormatRule { return !hasFoundDocComment ? decl : replaceTrivia( on: decl, - token: decl.firstToken, + token: decl.firstToken(viewMode: .sourceAccurate), leadingTrivia: Trivia(pieces: pieces.reversed()) ) } diff --git a/Sources/SwiftFormatRules/UseWhereClausesInForLoops.swift b/Sources/SwiftFormatRules/UseWhereClausesInForLoops.swift index 415929be7..25365e629 100644 --- a/Sources/SwiftFormatRules/UseWhereClausesInForLoops.swift +++ b/Sources/SwiftFormatRules/UseWhereClausesInForLoops.swift @@ -100,7 +100,7 @@ fileprivate func updateWithWhereCondition( statements: CodeBlockItemListSyntax ) -> ForInStmtSyntax { // Construct a new `where` clause with the condition. - let lastToken = node.sequenceExpr.lastToken + let lastToken = node.sequenceExpr.lastToken(viewMode: .sourceAccurate) var whereLeadingTrivia = Trivia() if lastToken?.trailingTrivia.containsSpaces == false { whereLeadingTrivia = .spaces(1) diff --git a/Sources/SwiftFormatRules/ValidateDocumentationComments.swift b/Sources/SwiftFormatRules/ValidateDocumentationComments.swift index cbf1bab18..c00921e8e 100644 --- a/Sources/SwiftFormatRules/ValidateDocumentationComments.swift +++ b/Sources/SwiftFormatRules/ValidateDocumentationComments.swift @@ -124,7 +124,7 @@ public final class ValidateDocumentationComments: SyntaxLintRule { let needsThrowsDesc = throwsOrRethrowsKeyword?.tokenKind == .keyword(.throws) if !needsThrowsDesc && throwsDesc != nil { - diagnose(.removeThrowsComment(funcName: name), on: throwsOrRethrowsKeyword ?? node.firstToken) + diagnose(.removeThrowsComment(funcName: name), on: throwsOrRethrowsKeyword ?? node.firstToken(viewMode: .sourceAccurate)) } else if needsThrowsDesc && throwsDesc == nil { diagnose(.documentErrorsThrown(funcName: name), on: throwsOrRethrowsKeyword) } From 7f58a910bd078ae0745cb7745dc1e2da82b41d06 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Sat, 15 Apr 2023 08:00:37 -0700 Subject: [PATCH 09/26] Merge pull request #512 from ahoppen/ahoppen/memberblock Adjustment for SwiftSyntax rename `members` -> `memberBlock` --- Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift index 358b3301e..382e31a44 100644 --- a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift @@ -267,7 +267,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { if let genericWhereClause = genericWhereClause { before(genericWhereClause.firstToken(viewMode: .sourceAccurate), tokens: .break(.same), .open) - after(members.leftBrace, tokens: .close) + after(memberBlock.leftBrace, tokens: .close) } let lastTokenBeforeBrace = inheritanceClause?.colon From 7e6fd10f4e69732dfeb8969e21c5b2795ece2e83 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Thu, 20 Apr 2023 23:56:07 -0700 Subject: [PATCH 10/26] Merge pull request #517 from ahoppen/ahoppen/argument-parser-dependency Change version dependency on `swift-argument-parser` to from `upToNextMinor` to `upToNextMajor` --- Package.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index e7448ec41..5bdae8fe5 100644 --- a/Package.swift +++ b/Package.swift @@ -213,7 +213,9 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { // Building standalone. package.dependencies += [ .package( - url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.2"), + url: "https://github.com/apple/swift-argument-parser.git", + from: "1.2.2" + ), .package( url: "https://github.com/apple/swift-syntax.git", branch: "release/5.9" From 7b329591fb4c9eaaa349adb52ab37e606b4b0247 Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Fri, 26 May 2023 09:31:20 -0700 Subject: [PATCH 11/26] Merge pull request #532 from allevato/multiline-string-fixes Fix indentation of multiline strings when part of a larger expression. --- .../TokenStreamCreator.swift | 23 ++++++- .../StringTests.swift | 68 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift index 382e31a44..d29b8697f 100644 --- a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift @@ -3298,7 +3298,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } /// Walks the expression and returns the leftmost multiline string literal (which might be the - /// expression itself) if the leftmost child is a multiline string literal. + /// expression itself) if the leftmost child is a multiline string literal or if it is a unary + /// operation applied to a multiline string literal. /// /// - Parameter expr: The expression whose leftmost multiline string literal should be returned. /// - Returns: The leftmost multiline string literal, or nil if the leftmost subexpression was @@ -3310,8 +3311,28 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { return stringLiteralExpr case .infixOperatorExpr(let infixOperatorExpr): return leftmostMultilineStringLiteral(of: infixOperatorExpr.leftOperand) + case .asExpr(let asExpr): + return leftmostMultilineStringLiteral(of: asExpr.expression) + case .isExpr(let isExpr): + return leftmostMultilineStringLiteral(of: isExpr.expression) + case .forcedValueExpr(let forcedValueExpr): + return leftmostMultilineStringLiteral(of: forcedValueExpr.expression) + case .optionalChainingExpr(let optionalChainingExpr): + return leftmostMultilineStringLiteral(of: optionalChainingExpr.expression) + case .postfixUnaryExpr(let postfixUnaryExpr): + return leftmostMultilineStringLiteral(of: postfixUnaryExpr.expression) + case .prefixOperatorExpr(let prefixOperatorExpr): + return leftmostMultilineStringLiteral(of: prefixOperatorExpr.postfixExpression) case .ternaryExpr(let ternaryExpr): return leftmostMultilineStringLiteral(of: ternaryExpr.conditionExpression) + case .functionCallExpr(let functionCallExpr): + return leftmostMultilineStringLiteral(of: functionCallExpr.calledExpression) + case .subscriptExpr(let subscriptExpr): + return leftmostMultilineStringLiteral(of: subscriptExpr.calledExpression) + case .memberAccessExpr(let memberAccessExpr): + return memberAccessExpr.base.flatMap { leftmostMultilineStringLiteral(of: $0) } + case .postfixIfConfigExpr(let postfixIfConfigExpr): + return postfixIfConfigExpr.base.flatMap { leftmostMultilineStringLiteral(of: $0) } default: return nil } diff --git a/Tests/SwiftFormatPrettyPrintTests/StringTests.swift b/Tests/SwiftFormatPrettyPrintTests/StringTests.swift index 13644ea0d..12129d5fe 100644 --- a/Tests/SwiftFormatPrettyPrintTests/StringTests.swift +++ b/Tests/SwiftFormatPrettyPrintTests/StringTests.swift @@ -327,4 +327,72 @@ final class StringTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 20) } + + func testLeadingMultilineStringsInOtherExpressions() { + // The stacked indentation behavior needs to drill down into different node types to find the + // leftmost multiline string literal. This makes sure that we cover various cases. + let input = + #""" + let bytes = """ + { + "key": "value" + } + """.utf8.count + let json = """ + { + "key": "value" + } + """.data(using: .utf8) + let slice = """ + { + "key": "value" + } + """[...] + let forceUnwrap = """ + { + "key": "value" + } + """! + let optionalChaining = """ + { + "key": "value" + } + """? + let postfix = """ + { + "key": "value" + } + """^*^ + let prefix = +""" + { + "key": "value" + } + """ + let postfixIf = """ + { + "key": "value" + } + """ + #if FLAG + .someMethod + #endif + + // Like the infix operator cases, cast operations force the string's open quotes to wrap. + // This could be considered consistent if you look at it through the right lens. Let's make + // sure to test it so that we can see if the behavior ever changes accidentally. + let cast = + """ + { + "key": "value" + } + """ as NSString + let typecheck = + """ + { + "key": "value" + } + """ is NSString + """# + assertPrettyPrintEqual(input: input, expected: input + "\n", linelength: 100) + } } From 4cd3d56ee854afb1f579256b1131eb8e7378544b Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Fri, 26 May 2023 12:14:39 -0700 Subject: [PATCH 12/26] Merge pull request #533 from allevato/no-assignment-fixes Fix `try`/`await` expressions in `NoAssignmentInExpressions`. --- .../NoAssignmentInExpressions.swift | 29 +++++++++++++++++-- .../NoAssignmentInExpressionsTests.swift | 19 ++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftFormatRules/NoAssignmentInExpressions.swift b/Sources/SwiftFormatRules/NoAssignmentInExpressions.swift index e327b59d7..0ae58473e 100644 --- a/Sources/SwiftFormatRules/NoAssignmentInExpressions.swift +++ b/Sources/SwiftFormatRules/NoAssignmentInExpressions.swift @@ -27,7 +27,7 @@ public final class NoAssignmentInExpressions: SyntaxFormatRule { public override func visit(_ node: InfixOperatorExprSyntax) -> ExprSyntax { // Diagnose any assignment that isn't directly a child of a `CodeBlockItem` (which would be the // case if it was its own statement). - if isAssignmentExpression(node) && node.parent?.is(CodeBlockItemSyntax.self) == false { + if isAssignmentExpression(node) && !isStandaloneAssignmentStatement(node) { diagnose(.moveAssignmentToOwnStatement, on: node) } return ExprSyntax(node) @@ -59,7 +59,8 @@ public final class NoAssignmentInExpressions: SyntaxFormatRule { item: .expr(ExprSyntax(assignmentExpr)), semicolon: nil ) - .with(\.leadingTrivia, + .with( + \.leadingTrivia, (returnStmt.leadingTrivia) + (assignmentExpr.leadingTrivia)) .with(\.trailingTrivia, [])) newItems.append( @@ -106,6 +107,30 @@ public final class NoAssignmentInExpressions: SyntaxFormatRule { return context.operatorTable.infixOperator(named: binaryOp.operatorToken.text)?.precedenceGroup == "AssignmentPrecedence" } + + /// Returns a value indicating whether the given node is a standalone assignment statement. + /// + /// This function considers try/await expressions and automatically walks up through them as + /// needed. This is because `try f().x = y` should still be a standalone assignment for our + /// purposes, even though a `TryExpr` will wrap the `InfixOperatorExpr` and thus would not be + /// considered a standalone assignment if we only checked the infix expression for a + /// `CodeBlockItem` parent. + private func isStandaloneAssignmentStatement(_ node: InfixOperatorExprSyntax) -> Bool { + var node = Syntax(node) + while + let parent = node.parent, + parent.is(TryExprSyntax.self) || parent.is(AwaitExprSyntax.self) + { + node = parent + } + + guard let parent = node.parent else { + // This shouldn't happen under normal circumstances (i.e., unless the expression is detached + // from the rest of a tree). In that case, we may as well consider it to be "standalone". + return true + } + return parent.is(CodeBlockItemSyntax.self) + } } extension Finding.Message { diff --git a/Tests/SwiftFormatRulesTests/NoAssignmentInExpressionsTests.swift b/Tests/SwiftFormatRulesTests/NoAssignmentInExpressionsTests.swift index 238c2f92f..dfeeefbf8 100644 --- a/Tests/SwiftFormatRulesTests/NoAssignmentInExpressionsTests.swift +++ b/Tests/SwiftFormatRulesTests/NoAssignmentInExpressionsTests.swift @@ -161,4 +161,23 @@ final class NoAssignmentInExpressionsTests: LintOrFormatRuleTestCase { ) XCTAssertDiagnosed(.moveAssignmentToOwnStatement, line: 2, column: 29) } + + func testTryAndAwaitAssignmentExpressionsAreUnchanged() { + XCTAssertFormatting( + NoAssignmentInExpressions.self, + input: """ + func foo() { + try a.b = c + await a.b = c + } + """, + expected: """ + func foo() { + try a.b = c + await a.b = c + } + """ + ) + XCTAssertNotDiagnosed(.moveAssignmentToOwnStatement) + } } From 254a14a17565b5a7f5c430b8cc82509624bd6012 Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Sat, 27 May 2023 08:56:04 -0700 Subject: [PATCH 13/26] Merge pull request #534 from allevato/more-multiline-string-fixes Further improve multiline string formatting. --- .../TokenStreamCreator.swift | 74 ++++++++++++++----- .../StringTests.swift | 38 ++++++++-- 2 files changed, 85 insertions(+), 27 deletions(-) diff --git a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift index d29b8697f..0efb74d6b 100644 --- a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift @@ -1883,10 +1883,13 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // If the rhs starts with a parenthesized expression, stack indentation around it. // Otherwise, use regular continuation breaks. - if let (unindentingNode, _, breakKind) = stackedIndentationBehavior(after: binOp, rhs: rhs) + if let (unindentingNode, _, breakKind, _) = + stackedIndentationBehavior(after: binOp, rhs: rhs) { beforeTokens = [.break(.open(kind: breakKind))] - after(unindentingNode.lastToken(viewMode: .sourceAccurate), tokens: [.break(.close(mustBreak: false), size: 0)]) + after( + unindentingNode.lastToken(viewMode: .sourceAccurate), + tokens: [.break(.close(mustBreak: false), size: 0)]) } else { beforeTokens = [.break(.continue)] } @@ -1894,13 +1897,13 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // When the RHS is a simple expression, even if is requires multiple lines, we don't add a // group so that as much of the expression as possible can stay on the same line as the // operator token. - if isCompoundExpression(rhs) { + if isCompoundExpression(rhs) && leftmostMultilineStringLiteral(of: rhs) == nil { beforeTokens.append(.open) after(rhs.lastToken(viewMode: .sourceAccurate), tokens: .close) } after(binOp.lastToken(viewMode: .sourceAccurate), tokens: beforeTokens) - } else if let (unindentingNode, shouldReset, breakKind) = + } else if let (unindentingNode, shouldReset, breakKind, shouldGroup) = stackedIndentationBehavior(after: binOp, rhs: rhs) { // For parenthesized expressions and for unparenthesized usages of `&&` and `||`, we don't @@ -1910,16 +1913,22 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // use open-continuation/close pairs around such operators and their right-hand sides so // that the continuation breaks inside those scopes "stack", instead of receiving the // usual single-level "continuation line or not" behavior. - let openBreakTokens: [Token] = [.break(.open(kind: breakKind)), .open] + var openBreakTokens: [Token] = [.break(.open(kind: breakKind))] + if shouldGroup { + openBreakTokens.append(.open) + } if wrapsBeforeOperator { before(binOp.firstToken(viewMode: .sourceAccurate), tokens: openBreakTokens) } else { after(binOp.lastToken(viewMode: .sourceAccurate), tokens: openBreakTokens) } - let closeBreakTokens: [Token] = + var closeBreakTokens: [Token] = (shouldReset ? [.break(.reset, size: 0)] : []) - + [.break(.close(mustBreak: false), size: 0), .close] + + [.break(.close(mustBreak: false), size: 0)] + if shouldGroup { + closeBreakTokens.append(.close) + } after(unindentingNode.lastToken(viewMode: .sourceAccurate), tokens: closeBreakTokens) } else { if wrapsBeforeOperator { @@ -2031,7 +2040,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { if let initializer = node.initializer { let expr = initializer.value - if let (unindentingNode, _, breakKind) = stackedIndentationBehavior(rhs: expr) { + if let (unindentingNode, _, breakKind, _) = stackedIndentationBehavior(rhs: expr) { after(initializer.equal, tokens: .break(.open(kind: breakKind))) after(unindentingNode.lastToken(viewMode: .sourceAccurate), tokens: .break(.close(mustBreak: false), size: 0)) } else { @@ -2042,7 +2051,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // When the RHS is a simple expression, even if is requires multiple lines, we don't add a // group so that as much of the expression as possible can stay on the same line as the // operator token. - if isCompoundExpression(expr) { + if isCompoundExpression(expr) && leftmostMultilineStringLiteral(of: expr) == nil { before(expr.firstToken(viewMode: .sourceAccurate), tokens: .open) after(expr.lastToken(viewMode: .sourceAccurate), tokens: .close) } @@ -3357,8 +3366,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } /// Determines if indentation should be stacked around a subexpression to the right of the given - /// operator, and, if so, returns the node after which indentation stacking should be closed and - /// whether or not the continuation state should be reset as well. + /// operator, and, if so, returns the node after which indentation stacking should be closed, + /// whether or not the continuation state should be reset as well, and whether or not a group + /// should be placed around the operator and the expression. /// /// Stacking is applied around parenthesized expressions, but also for low-precedence operators /// that frequently occur in long chains, such as logical AND (`&&`) and OR (`||`) in conditional @@ -3367,7 +3377,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { private func stackedIndentationBehavior( after operatorExpr: ExprSyntax? = nil, rhs: ExprSyntax - ) -> (unindentingNode: Syntax, shouldReset: Bool, breakKind: OpenBreakKind)? { + ) -> (unindentingNode: Syntax, shouldReset: Bool, breakKind: OpenBreakKind, shouldGroup: Bool)? { // Check for logical operators first, and if it's that kind of operator, stack indentation // around the entire right-hand-side. We have to do this check before checking the RHS for // parentheses because if the user writes something like `... && (foo) > bar || ...`, we don't @@ -3387,9 +3397,18 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // the paren to the last token of `rhs`. if let unindentingParenExpr = outermostEnclosingNode(from: Syntax(rhs)) { return ( - unindentingNode: unindentingParenExpr, shouldReset: true, breakKind: .continuation) + unindentingNode: unindentingParenExpr, + shouldReset: true, + breakKind: .continuation, + shouldGroup: true + ) } - return (unindentingNode: Syntax(rhs), shouldReset: true, breakKind: .continuation) + return ( + unindentingNode: Syntax(rhs), + shouldReset: true, + breakKind: .continuation, + shouldGroup: true + ) } } @@ -3399,8 +3418,11 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // We don't try to absorb any parens in this case, because the condition of a ternary cannot // be grouped with any exprs outside of the condition. return ( - unindentingNode: Syntax(ternaryExpr.conditionExpression), shouldReset: false, - breakKind: .continuation) + unindentingNode: Syntax(ternaryExpr.conditionExpression), + shouldReset: false, + breakKind: .continuation, + shouldGroup: true + ) } // If the right-hand-side of the operator is or starts with a parenthesized expression, stack @@ -3411,7 +3433,12 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // paren into the right hand side by unindenting after the final closing paren. This glues the // paren to the last token of `rhs`. if let unindentingParenExpr = outermostEnclosingNode(from: Syntax(rhs)) { - return (unindentingNode: unindentingParenExpr, shouldReset: true, breakKind: .continuation) + return ( + unindentingNode: unindentingParenExpr, + shouldReset: true, + breakKind: .continuation, + shouldGroup: true + ) } if let innerExpr = parenthesizedExpr.elementList.first?.expression, @@ -3423,14 +3450,23 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } return ( - unindentingNode: Syntax(parenthesizedExpr), shouldReset: false, breakKind: .continuation) + unindentingNode: Syntax(parenthesizedExpr), + shouldReset: false, + breakKind: .continuation, + shouldGroup: true + ) } // If the expression is a multiline string that is unparenthesized, create a block-based // indentation scope and have the segments aligned inside it. if let stringLiteralExpr = leftmostMultilineStringLiteral(of: rhs) { pendingMultilineStringBreakKinds[stringLiteralExpr] = .same - return (unindentingNode: Syntax(stringLiteralExpr), shouldReset: false, breakKind: .block) + return ( + unindentingNode: Syntax(stringLiteralExpr), + shouldReset: false, + breakKind: .block, + shouldGroup: false + ) } // Otherwise, don't stack--use regular continuation breaks instead. diff --git a/Tests/SwiftFormatPrettyPrintTests/StringTests.swift b/Tests/SwiftFormatPrettyPrintTests/StringTests.swift index 12129d5fe..271a83606 100644 --- a/Tests/SwiftFormatPrettyPrintTests/StringTests.swift +++ b/Tests/SwiftFormatPrettyPrintTests/StringTests.swift @@ -296,10 +296,35 @@ final class StringTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 20) } + func testMultilineStringsInExpressionWithNarrowMargins() { + let input = + #""" + x = """ + abcdefg + hijklmn + """ + """ + abcde + hijkl + """ + """# + + let expected = + #""" + x = """ + abcdefg + hijklmn + """ + + """ + abcde + hijkl + """ + + """# + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 9) + } + func testMultilineStringsInExpression() { - // This output could probably be improved, but it's also a fairly unlikely occurrence. The - // important part of this test is that the first string in the expression is indented relative - // to the `let`. let input = #""" let x = """ @@ -313,12 +338,10 @@ final class StringTests: PrettyPrintTestCase { let expected = #""" - let x = - """ + let x = """ this is a multiline string - """ - + """ + """ + """ this is more multiline string """ @@ -327,7 +350,6 @@ final class StringTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 20) } - func testLeadingMultilineStringsInOtherExpressions() { // The stacked indentation behavior needs to drill down into different node types to find the // leftmost multiline string literal. This makes sure that we cover various cases. From 3790e9d1aa6d9727b9549c7ff348232af8b1dc16 Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Sun, 28 May 2023 09:00:42 -0700 Subject: [PATCH 14/26] Merge pull request #535 from allevato/xctnothrow-assignment-fixes Allow exceptions to `NoAssignmentInExpressions`. --- .../Configuration.swift | 21 +++++++++++++ .../NoAssignmentInExpressions.swift | 30 +++++++++++++++++- .../NoAssignmentInExpressionsTests.swift | 31 +++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftFormatConfiguration/Configuration.swift b/Sources/SwiftFormatConfiguration/Configuration.swift index f698a3982..f5caaaabc 100644 --- a/Sources/SwiftFormatConfiguration/Configuration.swift +++ b/Sources/SwiftFormatConfiguration/Configuration.swift @@ -36,6 +36,7 @@ public struct Configuration: Codable, Equatable { case indentSwitchCaseLabels case rules case spacesAroundRangeFormationOperators + case noAssignmentInExpressions } /// The version of this configuration. @@ -147,6 +148,9 @@ public struct Configuration: Codable, Equatable { /// `...` and `..<`. public var spacesAroundRangeFormationOperators = false + /// Contains exceptions for the `NoAssignmentInExpressions` rule. + public var noAssignmentInExpressions = NoAssignmentInExpressionsConfiguration() + /// Constructs a Configuration with all default values. public init() { self.version = highestSupportedConfigurationVersion @@ -208,6 +212,10 @@ public struct Configuration: Codable, Equatable { ?? FileScopedDeclarationPrivacyConfiguration() self.indentSwitchCaseLabels = try container.decodeIfPresent(Bool.self, forKey: .indentSwitchCaseLabels) ?? false + self.noAssignmentInExpressions = + try container.decodeIfPresent( + NoAssignmentInExpressionsConfiguration.self, forKey: .noAssignmentInExpressions) + ?? NoAssignmentInExpressionsConfiguration() // 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 @@ -238,6 +246,7 @@ public struct Configuration: Codable, Equatable { spacesAroundRangeFormationOperators, forKey: .spacesAroundRangeFormationOperators) try container.encode(fileScopedDeclarationPrivacy, forKey: .fileScopedDeclarationPrivacy) try container.encode(indentSwitchCaseLabels, forKey: .indentSwitchCaseLabels) + try container.encode(noAssignmentInExpressions, forKey: .noAssignmentInExpressions) try container.encode(rules, forKey: .rules) } @@ -287,3 +296,15 @@ public struct FileScopedDeclarationPrivacyConfiguration: Codable, Equatable { /// private access. public var accessLevel: AccessLevel = .private } + +/// Configuration for the `NoAssignmentInExpressions` rule. +public struct NoAssignmentInExpressionsConfiguration: Codable, Equatable { + /// A list of function names where assignments are allowed to be embedded in expressions that are + /// passed as parameters to that function. + public var allowedFunctions: [String] = [ + // Allow `XCTAssertNoThrow` because `XCTAssertNoThrow(x = try ...)` is clearer about intent than + // `x = try XCTUnwrap(try? ...)` or force-unwrapped if you need to use the value `x` later on + // in the test. + "XCTAssertNoThrow" + ] +} diff --git a/Sources/SwiftFormatRules/NoAssignmentInExpressions.swift b/Sources/SwiftFormatRules/NoAssignmentInExpressions.swift index 0ae58473e..7384c5f36 100644 --- a/Sources/SwiftFormatRules/NoAssignmentInExpressions.swift +++ b/Sources/SwiftFormatRules/NoAssignmentInExpressions.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------===// +import SwiftFormatConfiguration import SwiftFormatCore import SwiftSyntax @@ -27,7 +28,10 @@ public final class NoAssignmentInExpressions: SyntaxFormatRule { public override func visit(_ node: InfixOperatorExprSyntax) -> ExprSyntax { // Diagnose any assignment that isn't directly a child of a `CodeBlockItem` (which would be the // case if it was its own statement). - if isAssignmentExpression(node) && !isStandaloneAssignmentStatement(node) { + if isAssignmentExpression(node) + && !isStandaloneAssignmentStatement(node) + && !isInAllowedFunction(node) + { diagnose(.moveAssignmentToOwnStatement, on: node) } return ExprSyntax(node) @@ -131,6 +135,30 @@ public final class NoAssignmentInExpressions: SyntaxFormatRule { } return parent.is(CodeBlockItemSyntax.self) } + + /// Returns true if the infix operator expression is in the (non-closure) parameters of an allowed + /// function call. + private func isInAllowedFunction(_ node: InfixOperatorExprSyntax) -> Bool { + let allowedFunctions = context.configuration.noAssignmentInExpressions.allowedFunctions + // Walk up the tree until we find a FunctionCallExprSyntax, and if the name matches, return + // true. However, stop early if we hit a CodeBlockItemSyntax first; this would represent a + // closure context where we *don't* want the exception to apply (for example, in + // `someAllowedFunction(a, b) { return c = d }`, the `c = d` is a descendent of a function call + // but we want it to be evaluated in its own context. + var node = Syntax(node) + while let parent = node.parent { + node = parent + if node.is(CodeBlockItemSyntax.self) { + break + } + if let functionCallExpr = node.as(FunctionCallExprSyntax.self), + allowedFunctions.contains(functionCallExpr.calledExpression.trimmedDescription) + { + return true + } + } + return false + } } extension Finding.Message { diff --git a/Tests/SwiftFormatRulesTests/NoAssignmentInExpressionsTests.swift b/Tests/SwiftFormatRulesTests/NoAssignmentInExpressionsTests.swift index dfeeefbf8..6ba554a0f 100644 --- a/Tests/SwiftFormatRulesTests/NoAssignmentInExpressionsTests.swift +++ b/Tests/SwiftFormatRulesTests/NoAssignmentInExpressionsTests.swift @@ -180,4 +180,35 @@ final class NoAssignmentInExpressionsTests: LintOrFormatRuleTestCase { ) XCTAssertNotDiagnosed(.moveAssignmentToOwnStatement) } + + func testAssignmentExpressionsInAllowedFunctions() { + XCTAssertFormatting( + NoAssignmentInExpressions.self, + input: """ + // These should not diagnose. + XCTAssertNoThrow(a = try b()) + XCTAssertNoThrow { a = try b() } + XCTAssertNoThrow({ a = try b() }) + someRegularFunction({ a = b }) + someRegularFunction { a = b } + + // This should be diagnosed. + someRegularFunction(a = b) + """, + expected: """ + // These should not diagnose. + XCTAssertNoThrow(a = try b()) + XCTAssertNoThrow { a = try b() } + XCTAssertNoThrow({ a = try b() }) + someRegularFunction({ a = b }) + someRegularFunction { a = b } + + // This should be diagnosed. + someRegularFunction(a = b) + """ + ) + XCTAssertDiagnosed(.moveAssignmentToOwnStatement, line: 9, column: 21) + // Make sure no other expressions were diagnosed. + XCTAssertNotDiagnosed(.moveAssignmentToOwnStatement) + } } From 4c612e249fa5995026b16d7a6642d1d752f5cb90 Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Thu, 8 Jun 2023 13:44:20 -0700 Subject: [PATCH 15/26] Merge pull request #539 from allevato/async-throws Fix `async throws` function types when they appear in an expression context. --- .../TokenStreamCreator.swift | 72 +++++----- .../ArrayDeclTests.swift | 57 +++++++- .../FunctionTypeTests.swift | 125 +++++++++++++++++- 3 files changed, 214 insertions(+), 40 deletions(-) diff --git a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift index 0efb74d6b..7414d62e3 100644 --- a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift @@ -412,6 +412,21 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { return .visitChildren } + override func visit(_ node: AccessorEffectSpecifiersSyntax) -> SyntaxVisitorContinueKind { + arrangeEffectSpecifiers(node) + return .visitChildren + } + + override func visit(_ node: FunctionEffectSpecifiersSyntax) -> SyntaxVisitorContinueKind { + arrangeEffectSpecifiers(node) + return .visitChildren + } + + override func visit(_ node: TypeEffectSpecifiersSyntax) -> SyntaxVisitorContinueKind { + arrangeEffectSpecifiers(node) + return .visitChildren + } + /// Applies formatting tokens to the tokens in the given function or function-like declaration /// node (e.g., initializers, deinitiailizers, and subscripts). private func arrangeFunctionLikeDecl( @@ -434,6 +449,17 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) } + /// Arranges the `async` and `throws` effect specifiers of a function or accessor declaration. + private func arrangeEffectSpecifiers(_ node: Node) { + before(node.asyncSpecifier, tokens: .break) + before(node.throwsSpecifier, tokens: .break) + // Keep them together if both `async` and `throws` are present. + if let asyncSpecifier = node.asyncSpecifier, let throwsSpecifier = node.throwsSpecifier { + before(asyncSpecifier, tokens: .open) + after(throwsSpecifier, tokens: .close) + } + } + // MARK: - Property and subscript accessor block nodes override func visit(_ node: AccessorListSyntax) -> SyntaxVisitorContinueKind { @@ -449,22 +475,6 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: AccessorDeclSyntax) -> SyntaxVisitorContinueKind { arrangeAttributeList(node.attributes) - - if let asyncKeyword = node.effectSpecifiers?.asyncSpecifier { - if node.effectSpecifiers?.throwsSpecifier != nil { - before(asyncKeyword, tokens: .break, .open) - } else { - before(asyncKeyword, tokens: .break) - } - } - - if let throwsKeyword = node.effectSpecifiers?.throwsSpecifier { - before(node.effectSpecifiers?.throwsSpecifier, tokens: .break) - if node.effectSpecifiers?.asyncSpecifier != nil { - after(throwsKeyword, tokens: .close) - } - } - arrangeBracesAndContents(of: node.body, contentsKeyPath: \.statements) return .visitChildren } @@ -1160,13 +1170,6 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } } - before(node.effectSpecifiers?.asyncSpecifier, tokens: .break) - before(node.effectSpecifiers?.throwsSpecifier, tokens: .break) - if let asyncKeyword = node.effectSpecifiers?.asyncSpecifier, let throwsTok = node.effectSpecifiers?.throwsSpecifier { - before(asyncKeyword, tokens: .open) - after(throwsTok, tokens: .close) - } - before(node.output?.arrow, tokens: .break) after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) before(node.inTok, tokens: .break(.same)) @@ -1606,8 +1609,6 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: FunctionTypeSyntax) -> SyntaxVisitorContinueKind { after(node.leftParen, tokens: .break(.open, size: 0), .open) before(node.rightParen, tokens: .break(.close, size: 0), .close) - before(node.effectSpecifiers?.asyncSpecifier, tokens: .break) - before(node.effectSpecifiers?.throwsSpecifier, tokens: .break) return .visitChildren } @@ -1832,14 +1833,6 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: FunctionSignatureSyntax) -> SyntaxVisitorContinueKind { - before(node.effectSpecifiers?.asyncSpecifier, tokens: .break) - before(node.effectSpecifiers?.throwsSpecifier, tokens: .break) - if let asyncOrReasyncKeyword = node.effectSpecifiers?.asyncSpecifier, - let throwsOrRethrowsKeyword = node.effectSpecifiers?.throwsSpecifier - { - before(asyncOrReasyncKeyword, tokens: .open) - after(throwsOrRethrowsKeyword, tokens: .close) - } before(node.output?.firstToken(viewMode: .sourceAccurate), tokens: .break) return .visitChildren } @@ -1872,6 +1865,14 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } let binOp = node.operatorOperand + if binOp.is(ArrowExprSyntax.self) { + // `ArrowExprSyntax` nodes occur when a function type is written in an expression context; + // for example, `let x = [(Int) throws -> Void]()`. We want to treat those consistently like + // we do other function return clauses and not treat them as regular binary operators, so + // handle that behavior there instead. + return .visitChildren + } + let rhs = node.rightOperand maybeGroupAroundSubexpression(rhs, combiningOperator: binOp) @@ -1985,9 +1986,8 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: ArrowExprSyntax) -> SyntaxVisitorContinueKind { - // The break before the `throws` keyword is inserted at the `InfixOperatorExpr` level so that it - // is placed in the correct relative position to the group surrounding the "operator". - after(node.effectSpecifiers?.throwsSpecifier, tokens: .break) + before(node.arrowToken, tokens: .break) + after(node.arrowToken, tokens: .space) return .visitChildren } diff --git a/Tests/SwiftFormatPrettyPrintTests/ArrayDeclTests.swift b/Tests/SwiftFormatPrettyPrintTests/ArrayDeclTests.swift index c0e8d6576..03b092915 100644 --- a/Tests/SwiftFormatPrettyPrintTests/ArrayDeclTests.swift +++ b/Tests/SwiftFormatPrettyPrintTests/ArrayDeclTests.swift @@ -70,17 +70,70 @@ final class ArrayDeclTests: PrettyPrintTestCase { let input = """ let A = [(Int, Double) -> Bool]() + let A = [(Int, Double) async -> Bool]() let A = [(Int, Double) throws -> Bool]() + let A = [(Int, Double) async throws -> Bool]() """ - let expected = + let expected46 = """ let A = [(Int, Double) -> Bool]() + let A = [(Int, Double) async -> Bool]() let A = [(Int, Double) throws -> Bool]() + let A = [(Int, Double) async throws -> Bool]() """ + assertPrettyPrintEqual(input: input, expected: expected46, linelength: 46) - assertPrettyPrintEqual(input: input, expected: expected, linelength: 45) + let expected43 = + """ + let A = [(Int, Double) -> Bool]() + let A = [(Int, Double) async -> Bool]() + let A = [(Int, Double) throws -> Bool]() + let A = [ + (Int, Double) async throws -> Bool + ]() + + """ + assertPrettyPrintEqual(input: input, expected: expected43, linelength: 43) + + let expected35 = + """ + let A = [(Int, Double) -> Bool]() + let A = [ + (Int, Double) async -> Bool + ]() + let A = [ + (Int, Double) throws -> Bool + ]() + let A = [ + (Int, Double) async throws + -> Bool + ]() + + """ + assertPrettyPrintEqual(input: input, expected: expected35, linelength: 35) + + let expected27 = + """ + let A = [ + (Int, Double) -> Bool + ]() + let A = [ + (Int, Double) async + -> Bool + ]() + let A = [ + (Int, Double) throws + -> Bool + ]() + let A = [ + (Int, Double) + async throws -> Bool + ]() + + """ + assertPrettyPrintEqual(input: input, expected: expected27, linelength: 27) } func testNoTrailingCommasInTypes() { diff --git a/Tests/SwiftFormatPrettyPrintTests/FunctionTypeTests.swift b/Tests/SwiftFormatPrettyPrintTests/FunctionTypeTests.swift index d3e78ee4c..8ee6370ee 100644 --- a/Tests/SwiftFormatPrettyPrintTests/FunctionTypeTests.swift +++ b/Tests/SwiftFormatPrettyPrintTests/FunctionTypeTests.swift @@ -60,6 +60,127 @@ final class FunctionTypeTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 60) } + func testFunctionTypeAsync() { + let input = + """ + func f(g: (_ somevalue: Int) async -> String?) { + let a = 123 + let b = "abc" + } + func f(g: (currentLevel: Int) async -> String?) { + let a = 123 + let b = "abc" + } + func f(g: (currentLevel: inout Int) async -> String?) { + let a = 123 + let b = "abc" + } + func f(g: (variable1: Int, variable2: Double, variable3: Bool) async -> Double) { + let a = 123 + let b = "abc" + } + func f(g: (variable1: Int, variable2: Double, variable3: Bool, variable4: String) async -> Double) { + let a = 123 + let b = "abc" + } + """ + + let expected = + """ + func f(g: (_ somevalue: Int) async -> String?) { + let a = 123 + let b = "abc" + } + func f(g: (currentLevel: Int) async -> String?) { + let a = 123 + let b = "abc" + } + func f(g: (currentLevel: inout Int) async -> String?) { + let a = 123 + let b = "abc" + } + func f( + g: (variable1: Int, variable2: Double, variable3: Bool) async -> + Double + ) { + let a = 123 + let b = "abc" + } + func f( + g: ( + variable1: Int, variable2: Double, variable3: Bool, + variable4: String + ) async -> Double + ) { + let a = 123 + let b = "abc" + } + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 66) + } + + func testFunctionTypeAsyncThrows() { + let input = + """ + func f(g: (_ somevalue: Int) async throws -> String?) { + let a = 123 + let b = "abc" + } + func f(g: (currentLevel: Int) async throws -> String?) { + let a = 123 + let b = "abc" + } + func f(g: (currentLevel: inout Int) async throws -> String?) { + let a = 123 + let b = "abc" + } + func f(g: (variable1: Int, variable2: Double, variable3: Bool) async throws -> Double) { + let a = 123 + let b = "abc" + } + func f(g: (variable1: Int, variable2: Double, variable3: Bool, variable4: String) async throws -> Double) { + let a = 123 + let b = "abc" + } + """ + + let expected = + """ + func f(g: (_ somevalue: Int) async throws -> String?) { + let a = 123 + let b = "abc" + } + func f(g: (currentLevel: Int) async throws -> String?) { + let a = 123 + let b = "abc" + } + func f(g: (currentLevel: inout Int) async throws -> String?) { + let a = 123 + let b = "abc" + } + func f( + g: (variable1: Int, variable2: Double, variable3: Bool) async throws -> + Double + ) { + let a = 123 + let b = "abc" + } + func f( + g: ( + variable1: Int, variable2: Double, variable3: Bool, variable4: String + ) async throws -> Double + ) { + let a = 123 + let b = "abc" + } + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 73) + } + func testFunctionTypeThrows() { let input = """ @@ -84,7 +205,7 @@ final class FunctionTypeTests: PrettyPrintTestCase { let b = "abc" } """ - + let expected = """ func f(g: (_ somevalue: Int) throws -> String?) { @@ -117,7 +238,7 @@ final class FunctionTypeTests: PrettyPrintTestCase { } """ - + assertPrettyPrintEqual(input: input, expected: expected, linelength: 67) } From 9ab832991fe8d83b09432b29977f031789cc4f94 Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Tue, 20 Jun 2023 08:22:24 -0700 Subject: [PATCH 16/26] Merge pull request #545 from allevato/keypath-wrapping Wrap keypath literals appropriately. --- .../TokenStreamCreator.swift | 42 +++++----- .../KeyPathExprTests.swift | 78 ++++++++++++++++--- 2 files changed, 92 insertions(+), 28 deletions(-) diff --git a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift index 7414d62e3..6ad067836 100644 --- a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift @@ -1759,6 +1759,30 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: KeyPathExprSyntax) -> SyntaxVisitorContinueKind { + before(node.backslash, tokens: .open) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) + return .visitChildren + } + + override func visit(_ node: KeyPathComponentSyntax) -> SyntaxVisitorContinueKind { + // If this is the first component (immediately after the backslash), allow a break after the + // slash only if a typename follows it. Do not break in the middle of `\.`. + var breakBeforePeriod = true + if let keyPathComponents = node.parent?.as(KeyPathComponentListSyntax.self), + let keyPathExpr = keyPathComponents.parent?.as(KeyPathExprSyntax.self), + node == keyPathExpr.components.first, keyPathExpr.root == nil + { + breakBeforePeriod = false + } + if breakBeforePeriod { + before(node.period, tokens: .break(.continue, size: 0)) + } + return .visitChildren + } + + override func visit(_ node: KeyPathSubscriptComponentSyntax) -> SyntaxVisitorContinueKind { + after(node.leftBracket, tokens: .break(.open, size: 0), .open) + before(node.rightBracket, tokens: .break(.close, size: 0), .close) return .visitChildren } @@ -1846,24 +1870,6 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: InfixOperatorExprSyntax) -> SyntaxVisitorContinueKind { - // FIXME: This is a workaround/hack for https://github.com/apple/swift-syntax/issues/928. For - // keypaths like `\.?.foo`, they get represented (after folding) as an infix operator expression - // with an empty keypath, followed by the "binary operator" `.?.`, followed by other - // expressions. We can detect this and treat the whole thing as a verbatim node, which mimics - // what we do today for keypaths (i.e., nothing). - if let keyPathExpr = node.leftOperand.as(KeyPathExprSyntax.self), - keyPathExpr.components.isEmpty - { - // If there were spaces in the trailing trivia of the previous token, they would have been - // ignored (since this expression would be expected to insert its own preceding breaks). - // Preserve that whitespace verbatim for now. - if let previousToken = node.firstToken(viewMode: .sourceAccurate)?.previousToken(viewMode: .sourceAccurate) { - appendTrailingTrivia(previousToken, forced: true) - } - verbatimToken(Syntax(node), indentingBehavior: .none) - return .skipChildren - } - let binOp = node.operatorOperand if binOp.is(ArrowExprSyntax.self) { // `ArrowExprSyntax` nodes occur when a function type is written in an expression context; diff --git a/Tests/SwiftFormatPrettyPrintTests/KeyPathExprTests.swift b/Tests/SwiftFormatPrettyPrintTests/KeyPathExprTests.swift index 77a41f146..e0ab55364 100644 --- a/Tests/SwiftFormatPrettyPrintTests/KeyPathExprTests.swift +++ b/Tests/SwiftFormatPrettyPrintTests/KeyPathExprTests.swift @@ -1,5 +1,3 @@ -// TODO: Add more tests and figure out how we want to wrap keypaths. Right now, they just get -// printed without breaks. final class KeyPathExprTests: PrettyPrintTestCase { func testSimple() { let input = @@ -47,7 +45,7 @@ final class KeyPathExprTests: PrettyPrintTestCase { let z = a.map(\.foo!.bar) """# - let expected = + let expected80 = #""" let x = \.foo? let y = \.foo!.bar @@ -55,7 +53,23 @@ final class KeyPathExprTests: PrettyPrintTestCase { """# - assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + assertPrettyPrintEqual(input: input, expected: expected80, linelength: 80) + + let expected11 = + #""" + let x = + \.foo? + let y = + \.foo! + .bar + let z = + a.map( + \.foo! + .bar) + + """# + + assertPrettyPrintEqual(input: input, expected: expected11, linelength: 11) } func testSubscript() { @@ -80,19 +94,63 @@ final class KeyPathExprTests: PrettyPrintTestCase { func testImplicitSelfUnwrap() { let input = #""" - //let x = \.?.foo - //let y = \.?.foo.bar + let x = \.?.foo + let y = \.?.foo.bar let z = a.map(\.?.foo.bar) """# - let expected = + let expected80 = #""" - //let x = \.?.foo - //let y = \.?.foo.bar + let x = \.?.foo + let y = \.?.foo.bar let z = a.map(\.?.foo.bar) """# - assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + assertPrettyPrintEqual(input: input, expected: expected80, linelength: 80) + + let expected11 = + #""" + let x = + \.?.foo + let y = + \.?.foo + .bar + let z = + a.map( + \.?.foo + .bar) + + """# + + assertPrettyPrintEqual(input: input, expected: expected11, linelength: 11) + } + + func testWrapping() { + let input = + #""" + let x = \ReallyLongType.reallyLongProperty.anotherLongProperty + let x = \.reeeeallyLongProperty.anotherLongProperty + let x = \.longProperty.a.b.c[really + long + expression].anotherLongProperty + """# + + let expected = + #""" + let x = + \ReallyLongType + .reallyLongProperty + .anotherLongProperty + let x = + \.reeeeallyLongProperty + .anotherLongProperty + let x = + \.longProperty.a.b.c[ + really + long + + expression + ].anotherLongProperty + + """# + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 23) } } From 93aade89dcda6fd7e7954ab718024f2957a3186a Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Tue, 20 Jun 2023 10:22:55 -0700 Subject: [PATCH 17/26] Merge pull request #546 from allevato/macro-decls Format `macro` declarations. --- .../TokenStreamCreator.swift | 46 ++ .../MacroDeclTests.swift | 397 ++++++++++++++++++ 2 files changed, 443 insertions(+) create mode 100644 Tests/SwiftFormatPrettyPrintTests/MacroDeclTests.swift diff --git a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift index 6ad067836..7f3807bde 100644 --- a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift @@ -240,6 +240,52 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { return .visitChildren } + override func visit(_ node: MacroDeclSyntax) -> SyntaxVisitorContinueKind { + // Macro declarations have a syntax that combines the best parts of types and functions while + // adding their own unique flavor, so we have to copy and adapt the relevant parts of those + // `arrange*` functions here. + before(node.firstToken(viewMode: .sourceAccurate), tokens: .open) + + arrangeAttributeList(node.attributes) + + let hasArguments = !node.signature.input.parameterList.isEmpty + + // Prioritize keeping ") -> " together. We can only do this if the macro has + // arguments. + if hasArguments && config.prioritizeKeepingFunctionOutputTogether { + // Due to visitation order, the matching .open break is added in ParameterClauseSyntax. + after(node.signature.lastToken(viewMode: .sourceAccurate), tokens: .close) + } + + let mustBreak = node.signature.output != nil || node.definition != nil + arrangeParameterClause(node.signature.input, forcesBreakBeforeRightParen: mustBreak) + + // Prioritize keeping " macro (" together. Also include the ")" if the + // parameter list is empty. + let firstTokenAfterAttributes = + node.modifiers?.firstToken(viewMode: .sourceAccurate) ?? node.macroKeyword + before(firstTokenAfterAttributes, tokens: .open) + after(node.macroKeyword, tokens: .break) + if hasArguments || node.genericParameterClause != nil { + after(node.signature.input.leftParen, tokens: .close) + } else { + after(node.signature.input.rightParen, tokens: .close) + } + + if let genericWhereClause = node.genericWhereClause { + before(genericWhereClause.firstToken(viewMode: .sourceAccurate), tokens: .break(.same), .open) + after(genericWhereClause.lastToken(viewMode: .sourceAccurate), tokens: .close) + } + if let definition = node.definition { + // Start the group *after* the `=` so that it all wraps onto its own line if it doesn't fit. + after(definition.equal, tokens: .open) + after(definition.lastToken(viewMode: .sourceAccurate), tokens: .close) + } + + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) + return .visitChildren + } + /// Applies formatting tokens to the tokens in the given type declaration node (i.e., a class, /// struct, enum, protocol, or extension). private func arrangeTypeDeclBlock( diff --git a/Tests/SwiftFormatPrettyPrintTests/MacroDeclTests.swift b/Tests/SwiftFormatPrettyPrintTests/MacroDeclTests.swift new file mode 100644 index 000000000..0529491c4 --- /dev/null +++ b/Tests/SwiftFormatPrettyPrintTests/MacroDeclTests.swift @@ -0,0 +1,397 @@ +import SwiftFormatConfiguration + +final class MacroDeclTests: PrettyPrintTestCase { + func testBasicMacroDeclarations_noPackArguments() { + let input = + """ + macro myFun(var1: Int, var2: Double) = #externalMacro(module: "Foo", type: "Bar") + macro reallyLongName(var1: Int, var2: Double, var3: Bool) = #externalMacro(module: "Foo", type: "Bar") + macro myFun() = #externalMacro(module: "Foo", type: "Bar") + """ + + let expected = + """ + macro myFun(var1: Int, var2: Double) = + #externalMacro(module: "Foo", type: "Bar") + macro reallyLongName( + var1: Int, + var2: Double, + var3: Bool + ) = #externalMacro(module: "Foo", type: "Bar") + macro myFun() = #externalMacro(module: "Foo", type: "Bar") + + """ + + var config = Configuration() + config.lineBreakBeforeEachArgument = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 58, configuration: config) + } + + func testBasicMacroDeclarations_packArguments() { + let input = + """ + macro myFun(var1: Int, var2: Double) = #externalMacro(module: "Foo", type: "Bar") + macro reallyLongName(var1: Int, var2: Double, var3: Bool) = #externalMacro(module: "Foo", type: "Bar") + macro myFun() = #externalMacro(module: "Foo", type: "Bar") + """ + + let expected = + """ + macro myFun(var1: Int, var2: Double) = + #externalMacro(module: "Foo", type: "Bar") + macro reallyLongName( + var1: Int, var2: Double, var3: Bool + ) = #externalMacro(module: "Foo", type: "Bar") + macro myFun() = #externalMacro(module: "Foo", type: "Bar") + + """ + + var config = Configuration() + config.lineBreakBeforeEachArgument = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 58, configuration: config) + } + + func testMacroDeclReturns() { + let input = + """ + macro myFun(var1: Int, var2: Double) -> Double = #externalMacro(module: "Foo", type: "Bar") + macro reallyLongName(var1: Int, var2: Double, var3: Bool) -> Double = #externalMacro(module: "Foo", type: "Bar") + macro reallyReallyLongName(var1: Int, var2: Double, var3: Bool) -> Double = #externalMacro(module: "Foo", type: "Bar") + macro tupleFunc() -> (one: Int, two: Double, three: Bool, four: String) = #externalMacro(module: "Foo", type: "Bar") + macro memberTypeReallyReallyLongNameFunc() -> Type.InnerMember = #externalMacro(module: "Foo", type: "Bar") + macro tupleMembersReallyLongNameFunc() -> (Type.Inner, Type2.Inner2) = #externalMacro(module: "Foo", type: "Bar") + """ + + let expected = + """ + macro myFun(var1: Int, var2: Double) -> Double = + #externalMacro(module: "Foo", type: "Bar") + macro reallyLongName(var1: Int, var2: Double, var3: Bool) + -> Double = #externalMacro(module: "Foo", type: "Bar") + macro reallyReallyLongName( + var1: Int, var2: Double, var3: Bool + ) -> Double = #externalMacro(module: "Foo", type: "Bar") + macro tupleFunc() -> ( + one: Int, two: Double, three: Bool, four: String + ) = #externalMacro(module: "Foo", type: "Bar") + macro memberTypeReallyReallyLongNameFunc() + -> Type.InnerMember = + #externalMacro(module: "Foo", type: "Bar") + macro tupleMembersReallyLongNameFunc() -> ( + Type.Inner, Type2.Inner2 + ) = #externalMacro(module: "Foo", type: "Bar") + + """ + + var config = Configuration() + config.lineBreakBeforeEachArgument = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 58, configuration: config) + } + + func testMacroGenericParameters_noPackArguments() { + let input = + """ + macro myFun(var1: S, var2: T) = #externalMacro(module: "Foo", type: "Bar") + macro myFun(var1: S) = #externalMacro(module: "Foo", type: "Bar") + macro longerNameFun(var1: ReallyLongTypeName, var2: TypeName) = #externalMacro(module: "Foo", type: "Bar") + """ + + let expected = + """ + macro myFun(var1: S, var2: T) = + #externalMacro(module: "Foo", type: "Bar") + macro myFun(var1: S) = + #externalMacro(module: "Foo", type: "Bar") + macro longerNameFun< + ReallyLongTypeName: Conform, + TypeName + >( + var1: ReallyLongTypeName, + var2: TypeName + ) = + #externalMacro(module: "Foo", type: "Bar") + + """ + + var config = Configuration() + config.lineBreakBeforeEachArgument = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 44, configuration: config) + } + + func testMacroGenericParameters_packArguments() { + let input = + """ + macro myFun(var1: S, var2: T) = #externalMacro(module: "Foo", type: "Bar") + macro myFun(var1: S) = #externalMacro(module: "Foo", type: "Bar") + macro longerNameFun(var1: ReallyLongTypeName, var2: TypeName) = #externalMacro(module: "Foo", type: "Bar") + """ + + let expected = + """ + macro myFun(var1: S, var2: T) = + #externalMacro(module: "Foo", type: "Bar") + macro myFun(var1: S) = + #externalMacro(module: "Foo", type: "Bar") + macro longerNameFun< + ReallyLongTypeName: Conform, TypeName + >( + var1: ReallyLongTypeName, var2: TypeName + ) = + #externalMacro(module: "Foo", type: "Bar") + + """ + + var config = Configuration() + config.lineBreakBeforeEachArgument = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 44, configuration: config) + } + + func testMacroWhereClause() { + let input = + """ + macro index( + of element: Element, in collection: Elements + ) -> Elements.Index? = #externalMacro(module: "Foo", type: "Bar") where Elements.Element == Element + + macro index( + of element: Element, + in collection: Elements + ) -> Elements.Index? = #externalMacro(module: "Foo", type: "Bar") where Elements.Element == Element, Element: Equatable + + macro index( + of element: Element, + in collection: Elements + ) -> Elements.Index? = #externalMacro(module: "Foo", type: "Bar") where Elements.Element == Element, Element: Equatable, Element: ReallyLongProtocolName + """ + + let expected = + """ + macro index( + of element: Element, in collection: Elements + ) -> Elements.Index? = + #externalMacro(module: "Foo", type: "Bar") + where Elements.Element == Element + + macro index( + of element: Element, + in collection: Elements + ) -> Elements.Index? = + #externalMacro(module: "Foo", type: "Bar") + where + Elements.Element == Element, Element: Equatable + + macro index( + of element: Element, + in collection: Elements + ) -> Elements.Index? = + #externalMacro(module: "Foo", type: "Bar") + where + Elements.Element == Element, Element: Equatable, + Element: ReallyLongProtocolName + + """ + + var config = Configuration() + config.lineBreakBeforeEachArgument = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 51, configuration: config) + } + + func testMacroWhereClause_lineBreakBeforeEachGenericRequirement() { + let input = + """ + public macro index( + of element: Element, in collection: Elements + ) -> Elements.Index? = #externalMacro(module: "Foo", type: "Bar") where Elements.Element == Element + + public macro index( + of element: Element, + in collection: Elements + ) -> Elements.Index? = #externalMacro(module: "Foo", type: "Bar") where Elements.Element == Element, Element: Equatable + + public macro index( + of element: Element, + in collection: Elements + ) -> Elements.Index? = #externalMacro(module: "Foo", type: "Bar") where Elements.Element == Element, Element: Equatable, Element: ReallyLongProtocolName + """ + + let expected = + """ + public macro index( + of element: Element, in collection: Elements + ) -> Elements.Index? = + #externalMacro(module: "Foo", type: "Bar") + where Elements.Element == Element + + public macro index( + of element: Element, + in collection: Elements + ) -> Elements.Index? = + #externalMacro(module: "Foo", type: "Bar") + where + Elements.Element == Element, + Element: Equatable + + public macro index( + of element: Element, + in collection: Elements + ) -> Elements.Index? = + #externalMacro(module: "Foo", type: "Bar") + where + Elements.Element == Element, + Element: Equatable, + Element: ReallyLongProtocolName + + """ + + var config = Configuration() + config.lineBreakBeforeEachArgument = false + config.lineBreakBeforeEachGenericRequirement = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 50, configuration: config) + } + + func testMacroAttributes() { + let input = + """ + @attached(accessor) public macro MyFun() = #externalMacro(module: "Foo", type: "Bar") + @attached(accessor) @attached(memberAttribute) public macro MyFun() = #externalMacro(module: "Foo", type: "Bar") + @attached(accessor) @attached(member, names: named(_storage)) public macro MyFun() = #externalMacro(module: "Foo", type: "Bar") + @attached(accessor) + @attached(member, names: named(_storage)) + public macro MyFun() = #externalMacro(module: "Foo", type: "Bar") + """ + + let expected = + """ + @attached(accessor) public macro MyFun() = + #externalMacro(module: "Foo", type: "Bar") + @attached(accessor) @attached(memberAttribute) public macro MyFun() = + #externalMacro(module: "Foo", type: "Bar") + @attached(accessor) @attached(member, names: named(_storage)) + public macro MyFun() = #externalMacro(module: "Foo", type: "Bar") + @attached(accessor) + @attached(member, names: named(_storage)) + public macro MyFun() = #externalMacro(module: "Foo", type: "Bar") + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 69) + } + + func testMacroDeclWithoutDefinition() { + let input = + """ + macro myFun() + macro myFun(arg1: Int) + macro myFun() -> Int + macro myFun(arg1: Int) + macro myFun(arg1: Int) where T: S + """ + + let expected = + """ + macro myFun() + macro myFun(arg1: Int) + macro myFun() -> Int + macro myFun(arg1: Int) + macro myFun(arg1: Int) where T: S + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 50) + } + + func testBreaksBeforeOrInsideOutput() { + let input = + """ + macro name(_ x: Int) -> R + """ + + let expected = + """ + macro name(_ x: Int) + -> R + + """ + assertPrettyPrintEqual(input: input, expected: expected, linelength: 23) + assertPrettyPrintEqual(input: input, expected: expected, linelength: 24) + assertPrettyPrintEqual(input: input, expected: expected, linelength: 27) + } + + func testBreaksBeforeOrInsideOutput_prioritizingKeepingOutputTogether() { + let input = + """ + macro name(_ x: Int) -> R + """ + + let expected = + """ + macro name( + _ x: Int + ) -> R + + """ + var config = Configuration() + config.prioritizeKeepingFunctionOutputTogether = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 23, configuration: config) + assertPrettyPrintEqual(input: input, expected: expected, linelength: 24, configuration: config) + assertPrettyPrintEqual(input: input, expected: expected, linelength: 27, configuration: config) + } + + func testBreaksBeforeOrInsideOutputWithAttributes() { + let input = + """ + @attached(member) @attached(memberAttribute) + macro name(_ x: Int) -> R + """ + + let expected = + """ + @attached(member) + @attached(memberAttribute) + macro name(_ x: Int) + -> R + + """ + assertPrettyPrintEqual(input: input, expected: expected, linelength: 26) + } + + func testBreaksBeforeOrInsideOutputWithAttributes_prioritizingKeepingOutputTogether() { + let input = + """ + @attached(member) @attached(memberAttribute) + macro name(_ x: Int) -> R + """ + + let expected = + """ + @attached(member) + @attached(memberAttribute) + macro name( + _ x: Int + ) -> R + + """ + var config = Configuration() + config.prioritizeKeepingFunctionOutputTogether = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 26, configuration: config) + } + + func testDoesNotBreakInsideEmptyParens() { + // If the macro name is so long that the parentheses of a no-argument parameter list would + // be pushed past the margin, don't break inside them. + let input = + """ + macro fooBarBaz() + + """ + + let expected = + """ + macro + fooBarBaz() + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 16) + } +} From dc4c97257734c09b3f72fe12bcb3339650dc57bb Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Tue, 20 Jun 2023 13:21:08 -0700 Subject: [PATCH 18/26] Merge pull request #544 from kimberninger/macro_expression_white_space Insert white space before trailing closure of a macro expression --- .../TokenStreamCreator.swift | 16 ++- .../MacroCallTests.swift | 117 ++++++++++++++++++ 2 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 Tests/SwiftFormatPrettyPrintTests/MacroCallTests.swift diff --git a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift index 7f3807bde..9353a4c70 100644 --- a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift @@ -1296,11 +1296,23 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: MacroExpansionExprSyntax) -> SyntaxVisitorContinueKind { + let arguments = node.argumentList + + // If there is a trailing closure, force the right parenthesis down to the next line so it + // stays with the open curly brace. + let breakBeforeRightParen = + (node.trailingClosure != nil && !isCompactSingleFunctionCallArgument(arguments)) + || mustBreakBeforeClosingDelimiter(of: node, argumentListPath: \.argumentList) + + before( + node.trailingClosure?.leftBrace, + tokens: .break(.same, newlines: .elective(ignoresDiscretionary: true))) + arrangeFunctionCallArgumentList( - node.argumentList, + arguments, leftDelimiter: node.leftParen, rightDelimiter: node.rightParen, - forcesBreakBeforeRightDelimiter: false) + forcesBreakBeforeRightDelimiter: breakBeforeRightParen) return .visitChildren } diff --git a/Tests/SwiftFormatPrettyPrintTests/MacroCallTests.swift b/Tests/SwiftFormatPrettyPrintTests/MacroCallTests.swift new file mode 100644 index 000000000..413190a41 --- /dev/null +++ b/Tests/SwiftFormatPrettyPrintTests/MacroCallTests.swift @@ -0,0 +1,117 @@ +import SwiftFormatConfiguration + +final class MacroCallTests: PrettyPrintTestCase { + func testNoWhiteSpaceAfterMacroWithoutTrailingClosure() { + let input = + """ + func myFunction() { + print("Currently running \\(#function)") + } + + """ + + let expected = + """ + func myFunction() { + print("Currently running \\(#function)") + } + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 50) + } + + func testKeepWhiteSpaceBeforeTrailingClosure() { + let input = + """ + #Preview {} + #Preview("MyPreview") { + MyView() + } + let p = #Predicate { $0 == 0 } + """ + + let expected = + """ + #Preview {} + #Preview("MyPreview") { + MyView() + } + let p = #Predicate { $0 == 0 } + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40) + } + + func testInsertWhiteSpaceBeforeTrailingClosure() { + let input = + """ + #Preview{} + #Preview("MyPreview"){ + MyView() + } + let p = #Predicate{ $0 == 0 } + """ + + let expected = + """ + #Preview {} + #Preview("MyPreview") { + MyView() + } + let p = #Predicate { $0 == 0 } + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40) + } + + func testDiscretionaryLineBreakBeforeTrailingClosure() { + let input = + """ + #Preview("MyPreview") + { + MyView() + } + #Preview( + "MyPreview", traits: .landscapeLeft + ) + { + MyView() + } + #Preview("MyPreview", traits: .landscapeLeft, .sizeThatFitsLayout) + { + MyView() + } + #Preview("MyPreview", traits: .landscapeLeft) { + MyView() + } + """ + + let expected = + """ + #Preview("MyPreview") { + MyView() + } + #Preview( + "MyPreview", traits: .landscapeLeft + ) { + MyView() + } + #Preview( + "MyPreview", traits: .landscapeLeft, + .sizeThatFitsLayout + ) { + MyView() + } + #Preview("MyPreview", traits: .landscapeLeft) + { + MyView() + } + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 45) + } +} From 189b4d5560af11a299632f83872d44410ae0bb61 Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Mon, 26 Jun 2023 04:35:58 -0700 Subject: [PATCH 19/26] Merge pull request #548 from allevato/unkNOwn-nodes Delete `UnknownNodeTests`. --- .../UnknownNodeTests.swift | 93 ------------------- 1 file changed, 93 deletions(-) delete mode 100644 Tests/SwiftFormatPrettyPrintTests/UnknownNodeTests.swift diff --git a/Tests/SwiftFormatPrettyPrintTests/UnknownNodeTests.swift b/Tests/SwiftFormatPrettyPrintTests/UnknownNodeTests.swift deleted file mode 100644 index 51f725cab..000000000 --- a/Tests/SwiftFormatPrettyPrintTests/UnknownNodeTests.swift +++ /dev/null @@ -1,93 +0,0 @@ -import XCTest - -/// Tests for unknown/malformed nodes that ensure that they are handled as verbatim text so that -/// their internal tokens do not get squashed together. -final class UnknownNodeTests: PrettyPrintTestCase { - func testUnknownDecl() throws { - throw XCTSkip("This is no longer an unknown declaration") - - let input = - """ - struct MyStruct where { - let a = 123 - } - """ - - assertPrettyPrintEqual(input: input, expected: input + "\n", linelength: 45) - } - - func testUnknownExpr() throws { - throw XCTSkip("This is no longer an unknown expression") - - let input = - """ - (foo where bar) - """ - - assertPrettyPrintEqual(input: input, expected: input + "\n", linelength: 45) - } - - func testUnknownPattern() throws { - let input = - """ - if case * ! = x { - bar() - } - """ - - let expected = - """ - if case - * ! = x - { - bar() - } - - """ - - assertPrettyPrintEqual(input: input, expected: expected, linelength: 45) - } - - func testUnknownStmt() throws { - throw XCTSkip("This is no longer an unknown statement") - - let input = - """ - if foo where { - } - """ - - assertPrettyPrintEqual(input: input, expected: input + "\n", linelength: 45) - } - - func testUnknownType() throws { - // This one loses the space after the colon because the break would normally be inserted before - // the first token in the type name. - let input = - """ - let x: where - """ - - let expected = - """ - let x:where - - """ - - assertPrettyPrintEqual(input: input, expected: expected, linelength: 45) - } - - func testNonEmptyTokenList() throws { - // The C++ parse modeled as a non-empty list of unparsed tokens. The Swift - // parser sees through this and treats it as an attribute with a missing - // name and some unexpected text after `foo!` in the arguments. - throw XCTSkip("This is no longer a non-empty token list") - - let input = - """ - @(foo ! @ # bar) - """ - - assertPrettyPrintEqual(input: input, expected: input + "\n", linelength: 45) - } -} From 85bf6cfa5ac48781f56226fcea254d70ad4dcdd2 Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Mon, 26 Jun 2023 13:48:26 -0700 Subject: [PATCH 20/26] Merge pull request #551 from allevato/empty-multiline-string Don't insert an extra break inside empty multiline strings. --- .../TokenStreamCreator.swift | 4 +- .../StringTests.swift | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift index 9353a4c70..0554fb16a 100644 --- a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift @@ -2286,7 +2286,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // Looks up the correct break kind based on prior context. let breakKind = pendingMultilineStringBreakKinds[node, default: .same] after(node.openQuote, tokens: .break(breakKind, size: 0, newlines: .hard(count: 1))) - before(node.closeQuote, tokens: .break(breakKind, newlines: .hard(count: 1))) + if !node.segments.isEmpty { + before(node.closeQuote, tokens: .break(breakKind, newlines: .hard(count: 1))) + } } return .visitChildren } diff --git a/Tests/SwiftFormatPrettyPrintTests/StringTests.swift b/Tests/SwiftFormatPrettyPrintTests/StringTests.swift index 271a83606..c31572648 100644 --- a/Tests/SwiftFormatPrettyPrintTests/StringTests.swift +++ b/Tests/SwiftFormatPrettyPrintTests/StringTests.swift @@ -350,6 +350,7 @@ final class StringTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 20) } + func testLeadingMultilineStringsInOtherExpressions() { // The stacked indentation behavior needs to drill down into different node types to find the // leftmost multiline string literal. This makes sure that we cover various cases. @@ -417,4 +418,44 @@ final class StringTests: PrettyPrintTestCase { """# assertPrettyPrintEqual(input: input, expected: input + "\n", linelength: 100) } + + func testEmptyMultilineStrings() { + let input = + ##""" + let x = """ + """ + let y = + """ + """ + let x = #""" + """# + let y = + #""" + """# + """## + + assertPrettyPrintEqual(input: input, expected: input + "\n", linelength: 20) + } + + func testOnlyBlankLinesMultilineStrings() { + let input = + ##""" + let x = """ + + """ + let y = + """ + + """ + let x = #""" + + """# + let y = + #""" + + """# + """## + + assertPrettyPrintEqual(input: input, expected: input + "\n", linelength: 20) + } } From 90c13900434bfcac03605019ac711cd4f8befb41 Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Tue, 27 Jun 2023 07:06:14 -0700 Subject: [PATCH 21/26] Merge pull request #547 from allevato/downgrade-placeholder-errors Downgrade `editor placeholder in source file` from error to warning. --- Sources/SwiftFormat/Parsing.swift | 39 +++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftFormat/Parsing.swift b/Sources/SwiftFormat/Parsing.swift index a74b32414..8b7c20adf 100644 --- a/Sources/SwiftFormat/Parsing.swift +++ b/Sources/SwiftFormat/Parsing.swift @@ -43,18 +43,53 @@ func parseAndEmitDiagnostics( operatorTable.foldAll(Parser.parse(source: source)) { _ in }.as(SourceFileSyntax.self)! let diagnostics = ParseDiagnosticsGenerator.diagnostics(for: sourceFile) + var hasErrors = false if let parsingDiagnosticHandler = parsingDiagnosticHandler { let expectedConverter = SourceLocationConverter(file: url?.path ?? "", tree: sourceFile) for diagnostic in diagnostics { let location = diagnostic.location(converter: expectedConverter) - parsingDiagnosticHandler(diagnostic, location) + + // Downgrade editor placeholders to warnings, because it is useful to support formatting + // in-progress files that contain those. + if diagnostic.diagnosticID == StaticTokenError.editorPlaceholder.diagnosticID { + parsingDiagnosticHandler(downgradedToWarning(diagnostic), location) + } else { + parsingDiagnosticHandler(diagnostic, location) + hasErrors = true + } } } - guard diagnostics.isEmpty else { + guard !hasErrors else { throw SwiftFormatError.fileContainsInvalidSyntax } return restoringLegacyTriviaBehavior(sourceFile) } + +// Wraps a `DiagnosticMessage` but forces its severity to be that of a warning instead of an error. +struct DowngradedDiagnosticMessage: DiagnosticMessage { + var originalDiagnostic: DiagnosticMessage + + var message: String { originalDiagnostic.message } + + var diagnosticID: SwiftDiagnostics.MessageID { originalDiagnostic.diagnosticID } + + var severity: DiagnosticSeverity { .warning } +} + +/// Returns a new `Diagnostic` that is identical to the given diagnostic, except that its severity +/// has been downgraded to a warning. +func downgradedToWarning(_ diagnostic: Diagnostic) -> Diagnostic { + // `Diagnostic` is immutable, so create a new one with the same values except for the + // severity-downgraded message. + return Diagnostic( + node: diagnostic.node, + position: diagnostic.position, + message: DowngradedDiagnosticMessage(originalDiagnostic: diagnostic.diagMessage), + highlights: diagnostic.highlights, + notes: diagnostic.notes, + fixIts: diagnostic.fixIts + ) +} From 6a8d51a2b6d8fc78c2e594a9a2a9a7ac39d16376 Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Tue, 27 Jun 2023 13:36:05 -0700 Subject: [PATCH 22/26] Merge pull request #553 from allevato/if-switch-exprs Improve wrapping of if/switch expressions. --- .../TokenStreamCreator.swift | 128 +++++++++++++----- .../IfStmtTests.swift | 48 ++++++- .../StringTests.swift | 27 ++++ .../SwitchStmtTests.swift | 40 ++++-- 4 files changed, 195 insertions(+), 48 deletions(-) diff --git a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift index 0554fb16a..b8b0f5bd5 100644 --- a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift @@ -1948,13 +1948,18 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { // If the rhs starts with a parenthesized expression, stack indentation around it. // Otherwise, use regular continuation breaks. - if let (unindentingNode, _, breakKind, _) = + if let (unindentingNode, _, breakKind, shouldGroup) = stackedIndentationBehavior(after: binOp, rhs: rhs) { beforeTokens = [.break(.open(kind: breakKind))] + var afterTokens: [Token] = [.break(.close(mustBreak: false), size: 0)] + if shouldGroup { + beforeTokens.append(.open) + afterTokens.append(.close) + } after( unindentingNode.lastToken(viewMode: .sourceAccurate), - tokens: [.break(.close(mustBreak: false), size: 0)]) + tokens: afterTokens) } else { beforeTokens = [.break(.continue)] } @@ -2104,9 +2109,17 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { if let initializer = node.initializer { let expr = initializer.value - if let (unindentingNode, _, breakKind, _) = stackedIndentationBehavior(rhs: expr) { - after(initializer.equal, tokens: .break(.open(kind: breakKind))) - after(unindentingNode.lastToken(viewMode: .sourceAccurate), tokens: .break(.close(mustBreak: false), size: 0)) + if let (unindentingNode, _, breakKind, shouldGroup) = stackedIndentationBehavior(rhs: expr) { + var openTokens: [Token] = [.break(.open(kind: breakKind))] + if shouldGroup { + openTokens.append(.open) + } + after(initializer.equal, tokens: openTokens) + var closeTokens: [Token] = [.break(.close(mustBreak: false), size: 0)] + if shouldGroup { + closeTokens.append(.close) + } + after(unindentingNode.lastToken(viewMode: .sourceAccurate), tokens: closeTokens) } else { after(initializer.equal, tokens: .break(.continue)) } @@ -2273,9 +2286,13 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: InitializerClauseSyntax) -> SyntaxVisitorContinueKind { before(node.equal, tokens: .space) - // InitializerClauses that are children of a PatternBindingSyntax are already handled in the - // latter node, to ensure that continuations stack appropriately. - if node.parent == nil || !node.parent!.is(PatternBindingSyntax.self) { + // InitializerClauses that are children of a PatternBindingSyntax or + // OptionalBindingConditionSyntax are already handled in the latter node, to ensure that + // continuations stack appropriately. + if let parent = node.parent, + !parent.is(PatternBindingSyntax.self) + && !parent.is(OptionalBindingConditionSyntax.self) + { after(node.equal, tokens: .break) } return .visitChildren @@ -2457,6 +2474,26 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { after(typeAnnotation.lastToken(viewMode: .sourceAccurate), tokens: .break(.close(mustBreak: false), size: 0)) } + if let initializer = node.initializer { + if let (unindentingNode, _, breakKind, shouldGroup) = + stackedIndentationBehavior(rhs: initializer.value) + { + var openTokens: [Token] = [.break(.open(kind: breakKind))] + if shouldGroup { + openTokens.append(.open) + } + after(initializer.equal, tokens: openTokens) + + var closeTokens: [Token] = [.break(.close(mustBreak: false), size: 0)] + if shouldGroup { + closeTokens.append(.close) + } + after(unindentingNode.lastToken(viewMode: .sourceAccurate), tokens: closeTokens) + } else { + after(initializer.equal, tokens: .break(.continue)) + } + } + return .visitChildren } @@ -3372,47 +3409,63 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } } - /// Walks the expression and returns the leftmost multiline string literal (which might be the - /// expression itself) if the leftmost child is a multiline string literal or if it is a unary - /// operation applied to a multiline string literal. + /// Walks the expression and returns the leftmost subexpression (which might be the expression + /// itself) if the leftmost child is a node of the given type or if it is a unary operation + /// applied to a node of the given type. /// - /// - Parameter expr: The expression whose leftmost multiline string literal should be returned. - /// - Returns: The leftmost multiline string literal, or nil if the leftmost subexpression was - /// not a multiline string literal. - private func leftmostMultilineStringLiteral(of expr: ExprSyntax) -> StringLiteralExprSyntax? { + /// - Parameter expr: The expression whose leftmost matching subexpression should be returned. + /// - Returns: The leftmost subexpression, or nil if the leftmost subexpression was not the + /// desired type. + private func leftmostExpr( + of expr: ExprSyntax, + ifMatching predicate: (ExprSyntax) -> Bool + ) -> ExprSyntax? { + if predicate(expr) { + return expr + } switch Syntax(expr).as(SyntaxEnum.self) { - case .stringLiteralExpr(let stringLiteralExpr) - where stringLiteralExpr.openQuote.tokenKind == .multilineStringQuote: - return stringLiteralExpr case .infixOperatorExpr(let infixOperatorExpr): - return leftmostMultilineStringLiteral(of: infixOperatorExpr.leftOperand) + return leftmostExpr(of: infixOperatorExpr.leftOperand, ifMatching: predicate) case .asExpr(let asExpr): - return leftmostMultilineStringLiteral(of: asExpr.expression) + return leftmostExpr(of: asExpr.expression, ifMatching: predicate) case .isExpr(let isExpr): - return leftmostMultilineStringLiteral(of: isExpr.expression) + return leftmostExpr(of: isExpr.expression, ifMatching: predicate) case .forcedValueExpr(let forcedValueExpr): - return leftmostMultilineStringLiteral(of: forcedValueExpr.expression) + return leftmostExpr(of: forcedValueExpr.expression, ifMatching: predicate) case .optionalChainingExpr(let optionalChainingExpr): - return leftmostMultilineStringLiteral(of: optionalChainingExpr.expression) + return leftmostExpr(of: optionalChainingExpr.expression, ifMatching: predicate) case .postfixUnaryExpr(let postfixUnaryExpr): - return leftmostMultilineStringLiteral(of: postfixUnaryExpr.expression) + return leftmostExpr(of: postfixUnaryExpr.expression, ifMatching: predicate) case .prefixOperatorExpr(let prefixOperatorExpr): - return leftmostMultilineStringLiteral(of: prefixOperatorExpr.postfixExpression) + return leftmostExpr(of: prefixOperatorExpr.postfixExpression, ifMatching: predicate) case .ternaryExpr(let ternaryExpr): - return leftmostMultilineStringLiteral(of: ternaryExpr.conditionExpression) + return leftmostExpr(of: ternaryExpr.conditionExpression, ifMatching: predicate) case .functionCallExpr(let functionCallExpr): - return leftmostMultilineStringLiteral(of: functionCallExpr.calledExpression) + return leftmostExpr(of: functionCallExpr.calledExpression, ifMatching: predicate) case .subscriptExpr(let subscriptExpr): - return leftmostMultilineStringLiteral(of: subscriptExpr.calledExpression) + return leftmostExpr(of: subscriptExpr.calledExpression, ifMatching: predicate) case .memberAccessExpr(let memberAccessExpr): - return memberAccessExpr.base.flatMap { leftmostMultilineStringLiteral(of: $0) } + return memberAccessExpr.base.flatMap { leftmostExpr(of: $0, ifMatching: predicate) } case .postfixIfConfigExpr(let postfixIfConfigExpr): - return postfixIfConfigExpr.base.flatMap { leftmostMultilineStringLiteral(of: $0) } + return postfixIfConfigExpr.base.flatMap { leftmostExpr(of: $0, ifMatching: predicate) } default: return nil } } + /// Walks the expression and returns the leftmost multiline string literal (which might be the + /// expression itself) if the leftmost child is a multiline string literal or if it is a unary + /// operation applied to a multiline string literal. + /// + /// - Parameter expr: The expression whose leftmost multiline string literal should be returned. + /// - Returns: The leftmost multiline string literal, or nil if the leftmost subexpression was + /// not a multiline string literal. + private func leftmostMultilineStringLiteral(of expr: ExprSyntax) -> StringLiteralExprSyntax? { + return leftmostExpr(of: expr) { + $0.as(StringLiteralExprSyntax.self)?.openQuote.tokenKind == .multilineStringQuote + }?.as(StringLiteralExprSyntax.self) + } + /// Returns the outermost node enclosing the given node whose closing delimiter(s) must be kept /// alongside the last token of the given node. Any tokens between `node.lastToken` and the /// returned node's `lastToken` are delimiter tokens that shouldn't be preceded by a break. @@ -3503,7 +3556,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { unindentingNode: unindentingParenExpr, shouldReset: true, breakKind: .continuation, - shouldGroup: true + shouldGroup: false ) } @@ -3519,7 +3572,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { unindentingNode: Syntax(parenthesizedExpr), shouldReset: false, breakKind: .continuation, - shouldGroup: true + shouldGroup: false ) } @@ -3535,6 +3588,17 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { ) } + if let leftmostExpr = leftmostExpr(of: rhs, ifMatching: { + $0.is(IfExprSyntax.self) || $0.is(SwitchExprSyntax.self) + }) { + return ( + unindentingNode: Syntax(leftmostExpr), + shouldReset: false, + breakKind: .block, + shouldGroup: true + ) + } + // Otherwise, don't stack--use regular continuation breaks instead. return nil } diff --git a/Tests/SwiftFormatPrettyPrintTests/IfStmtTests.swift b/Tests/SwiftFormatPrettyPrintTests/IfStmtTests.swift index a67dfc754..c9320884b 100644 --- a/Tests/SwiftFormatPrettyPrintTests/IfStmtTests.swift +++ b/Tests/SwiftFormatPrettyPrintTests/IfStmtTests.swift @@ -206,13 +206,14 @@ final class IfStmtTests: PrettyPrintTestCase { let expected = """ func foo() -> Int { - let x = if var1 < var2 { - 23 - } else if d < e { - 24 - } else { - 0 - } + let x = + if var1 < var2 { + 23 + } else if d < e { + 24 + } else { + 0 + } return x } @@ -221,6 +222,39 @@ final class IfStmtTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 26) } + func testIfExpression3() { + let input = + """ + let x = if a { b } else { c } + xyzab = if a { b } else { c } + """ + assertPrettyPrintEqual(input: input, expected: input + "\n", linelength: 80) + + let expected28 = + """ + let x = + if a { b } else { c } + xyzab = + if a { b } else { c } + + """ + assertPrettyPrintEqual(input: input, expected: expected28, linelength: 28) + + let expected22 = + """ + let x = + if a { b } else { + c + } + xyzab = + if a { b } else { + c + } + + """ + assertPrettyPrintEqual(input: input, expected: expected22, linelength: 22) + } + func testMatchingPatternConditions() { let input = """ diff --git a/Tests/SwiftFormatPrettyPrintTests/StringTests.swift b/Tests/SwiftFormatPrettyPrintTests/StringTests.swift index c31572648..d82aba30c 100644 --- a/Tests/SwiftFormatPrettyPrintTests/StringTests.swift +++ b/Tests/SwiftFormatPrettyPrintTests/StringTests.swift @@ -419,6 +419,33 @@ final class StringTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: input + "\n", linelength: 100) } + func testMultilineStringsNestedInAnotherWrappingContext() { + let input = + #""" + guard + let x = """ + blah + blah + """.data(using: .utf8) { + print(x) + } + """# + + let expected = + #""" + guard + let x = """ + blah + blah + """.data(using: .utf8) + { + print(x) + } + + """# + assertPrettyPrintEqual(input: input, expected: expected, linelength: 100) + } + func testEmptyMultilineStrings() { let input = ##""" diff --git a/Tests/SwiftFormatPrettyPrintTests/SwitchStmtTests.swift b/Tests/SwiftFormatPrettyPrintTests/SwitchStmtTests.swift index 08e07d94e..c2b744a1c 100644 --- a/Tests/SwiftFormatPrettyPrintTests/SwitchStmtTests.swift +++ b/Tests/SwiftFormatPrettyPrintTests/SwitchStmtTests.swift @@ -297,20 +297,42 @@ final class SwitchStmtTests: PrettyPrintTestCase { let expected = """ func foo() -> Int { - let x = switch value1 + value2 + value3 + value4 { - case "a": - 0 - case "b": - 1 - default: - 2 - } + let x = + switch value1 + value2 + value3 + value4 { + case "a": + 0 + case "b": + 1 + default: + 2 + } + return x + } + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 46) + + let expected43 = + """ + func foo() -> Int { + let x = + switch value1 + value2 + value3 + + value4 + { + case "a": + 0 + case "b": + 1 + default: + 2 + } return x } """ - assertPrettyPrintEqual(input: input, expected: expected, linelength: 52) + assertPrettyPrintEqual(input: input, expected: expected43, linelength: 43) } func testUnknownDefault() { From 23068d453619f4ae62e892d3b406862a5d8fa28d Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Wed, 28 Jun 2023 13:22:16 -0700 Subject: [PATCH 23/26] Merge pull request #554 from allevato/poundif-after-paren Fix postfix-`#if` formatting when they come after a closing parenthesis. --- .../TokenStreamCreator.swift | 72 ++++++------- .../IfConfigTests.swift | 102 ++++++++++++++---- 2 files changed, 116 insertions(+), 58 deletions(-) diff --git a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift index b8b0f5bd5..7341f4288 100644 --- a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift @@ -988,7 +988,6 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: MemberAccessExprSyntax) -> SyntaxVisitorContinueKind { preVisitInsertingContextualBreaks(node) - return .visitChildren } @@ -996,6 +995,15 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { clearContextualBreakState(node) } + override func visit(_ node: PostfixIfConfigExprSyntax) -> SyntaxVisitorContinueKind { + preVisitInsertingContextualBreaks(node) + return .visitChildren + } + + override func visitPost(_ node: PostfixIfConfigExprSyntax) { + clearContextualBreakState(node) + } + override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind { preVisitInsertingContextualBreaks(node) @@ -1456,29 +1464,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { before(tokenToOpenWith.nextToken(viewMode: .all), tokens: .break(breakKindClose, newlines: .soft), .close) } - if isNestedInPostfixIfConfig(node: Syntax(node)) { - let breakToken: Token - let currentIfConfigDecl = node.parent?.parent?.as(IfConfigDeclSyntax.self) - - if let currentIfConfigDecl = currentIfConfigDecl, - let tokenBeforeCurrentIfConfigDecl = currentIfConfigDecl.previousToken(viewMode: .sourceAccurate), - isNestedInIfConfig(node: Syntax(tokenBeforeCurrentIfConfigDecl)) || - tokenBeforeCurrentIfConfigDecl.text == "}" { - breakToken = .break(.reset) - } else { - breakToken = .break - before(currentIfConfigDecl?.poundEndif, tokens: [.break]) - } - + if !isNestedInPostfixIfConfig(node: Syntax(node)), let condition = node.condition { before( - node.firstToken(viewMode: .sourceAccurate), - tokens: [ - .printerControl(kind: .enableBreaking), - breakToken, - ] - ) - } else if let condition = node.condition { - before(condition.firstToken(viewMode: .sourceAccurate), tokens: .printerControl(kind: .disableBreaking(allowDiscretionary: true))) + condition.firstToken(viewMode: .sourceAccurate), + tokens: .printerControl(kind: .disableBreaking(allowDiscretionary: true))) after( condition.lastToken(viewMode: .sourceAccurate), tokens: .printerControl(kind: .enableBreaking), .break(.reset, size: 0)) @@ -3287,7 +3276,11 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { private func mustBreakBeforeClosingDelimiter( of expr: T, argumentListPath: KeyPath ) -> Bool { - guard let parent = expr.parent, parent.is(MemberAccessExprSyntax.self) else { return false } + guard + let parent = expr.parent, + parent.is(MemberAccessExprSyntax.self) || parent.is(PostfixIfConfigExprSyntax.self) + else { return false } + let argumentList = expr[keyPath: argumentListPath] // When there's a single compact argument, there is no extra indentation for the argument and @@ -3722,6 +3715,23 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { after(expr.lastToken(viewMode: .sourceAccurate), tokens: .contextualBreakingEnd) } return (hasCompoundExpression, true) + } else if let postfixIfExpr = expr.as(PostfixIfConfigExprSyntax.self), + let base = postfixIfExpr.base + { + // For postfix-if expressions with bases (i.e., they aren't the first `#if` nested inside + // another `#if`), add contextual breaks before the top-level clauses (and the terminating + // `#endif`) so that they nest or line-up properly based on the preceding node. We don't do + // this for initial nested `#if`s because they will already get open/close breaks to control + // their indentation from their parent clause. + before(postfixIfExpr.firstToken(viewMode: .sourceAccurate), tokens: .contextualBreakingStart) + after(postfixIfExpr.lastToken(viewMode: .sourceAccurate), tokens: .contextualBreakingEnd) + + for clause in postfixIfExpr.config.clauses { + before(clause.poundKeyword, tokens: .break(.contextual, size: 0)) + } + before(postfixIfExpr.config.poundEndif, tokens: .break(.contextual, size: 0)) + + return insertContextualBreaks(base, isTopLevel: false) } else if let callingExpr = expr.asProtocol(CallingExprSyntaxProtocol.self) { let calledExpression = callingExpr.calledExpression let (hasCompoundExpression, hasMemberAccess) = @@ -3788,20 +3798,6 @@ private func isNestedInPostfixIfConfig(node: Syntax) -> Bool { return false } -private func isNestedInIfConfig(node: Syntax) -> Bool { - var this: Syntax? = node - - while this?.parent != nil { - if this?.is(IfConfigClauseSyntax.self) == true { - return true - } - - this = this?.parent - } - - return false -} - extension Syntax { /// Creates a pretty-printable token stream for the provided Syntax node. func makeTokenStream(configuration: Configuration, operatorTable: OperatorTable) -> [Token] { diff --git a/Tests/SwiftFormatPrettyPrintTests/IfConfigTests.swift b/Tests/SwiftFormatPrettyPrintTests/IfConfigTests.swift index 9a01fe631..5e8e1199f 100644 --- a/Tests/SwiftFormatPrettyPrintTests/IfConfigTests.swift +++ b/Tests/SwiftFormatPrettyPrintTests/IfConfigTests.swift @@ -163,25 +163,25 @@ final class IfConfigTests: PrettyPrintTestCase { func testInvalidDiscretionaryLineBreaksRemoved() { let input = - """ - #if (canImport(SwiftUI) && - !(os(iOS) && - arch(arm)) && - ((canImport(AppKit) || - canImport(UIKit)) && !os(watchOS))) - conditionalFunc(foo, bar, baz) - #endif - """ - - let expected = - """ - #if (canImport(SwiftUI) && !(os(iOS) && arch(arm)) && ((canImport(AppKit) || canImport(UIKit)) && !os(watchOS))) - conditionalFunc(foo, bar, baz) - #endif - - """ - - assertPrettyPrintEqual(input: input, expected: expected, linelength: 40) + """ + #if (canImport(SwiftUI) && + !(os(iOS) && + arch(arm)) && + ((canImport(AppKit) || + canImport(UIKit)) && !os(watchOS))) + conditionalFunc(foo, bar, baz) + #endif + """ + + let expected = + """ + #if (canImport(SwiftUI) && !(os(iOS) && arch(arm)) && ((canImport(AppKit) || canImport(UIKit)) && !os(watchOS))) + conditionalFunc(foo, bar, baz) + #endif + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 40) } func testValidDiscretionaryLineBreaksRetained() { @@ -299,6 +299,8 @@ final class IfConfigTests: PrettyPrintTestCase { #if os(iOS) || os(watchOS) #if os(iOS) .iOSModifier() + #elseif os(tvOS) + .tvOSModifier() #else .watchOSModifier() #endif @@ -314,6 +316,8 @@ final class IfConfigTests: PrettyPrintTestCase { #if os(iOS) || os(watchOS) #if os(iOS) .iOSModifier() + #elseif os(tvOS) + .tvOSModifier() #else .watchOSModifier() #endif @@ -326,7 +330,6 @@ final class IfConfigTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 45) } - func testPostfixPoundIfAfterVariables() { let input = """ @@ -398,6 +401,7 @@ final class IfConfigTests: PrettyPrintTestCase { .padding([.vertical]) #if os(iOS) .iOSSpecificModifier() + .anotherIOSSpecificModifier() #endif .commonModifier() """ @@ -408,6 +412,7 @@ final class IfConfigTests: PrettyPrintTestCase { .padding([.vertical]) #if os(iOS) .iOSSpecificModifier() + .anotherIOSSpecificModifier() #endif .commonModifier() @@ -454,4 +459,61 @@ final class IfConfigTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 45) } + + func testPostfixPoundIfNotIndentedIfClosingParenOnOwnLine() { + let input = + """ + SomeFunction( + foo, + bar + ) + #if os(iOS) + .iOSSpecificModifier() + #endif + .commonModifier() + """ + + let expected = + """ + SomeFunction( + foo, + bar + ) + #if os(iOS) + .iOSSpecificModifier() + #endif + .commonModifier() + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 45) + } + + func testPostfixPoundIfForcesPrecedingClosingParenOntoNewLine() { + let input = + """ + SomeFunction( + foo, + bar) + #if os(iOS) + .iOSSpecificModifier() + #endif + .commonModifier() + """ + + let expected = + """ + SomeFunction( + foo, + bar + ) + #if os(iOS) + .iOSSpecificModifier() + #endif + .commonModifier() + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 45) + } } From 3d99b22d28add9bcd746f28a1a2e1bdd73373e32 Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Thu, 29 Jun 2023 04:55:46 -0700 Subject: [PATCH 24/26] Merge pull request #555 from allevato/multiline-raw-values Fix indentation of multiline strings in enum case raw values. --- .../TokenStreamCreator.swift | 23 ++++++++++++++++++- .../StringTests.swift | 11 +++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift index 7341f4288..8e98b722a 100644 --- a/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift @@ -1544,6 +1544,26 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { arrangeEnumCaseParameterClause(associatedValue, forcesBreakBeforeRightParen: false) } + if let initializer = node.rawValue { + if let (unindentingNode, _, breakKind, shouldGroup) = + stackedIndentationBehavior(rhs: initializer.value) + { + var openTokens: [Token] = [.break(.open(kind: breakKind))] + if shouldGroup { + openTokens.append(.open) + } + after(initializer.equal, tokens: openTokens) + + var closeTokens: [Token] = [.break(.close(mustBreak: false), size: 0)] + if shouldGroup { + closeTokens.append(.close) + } + after(unindentingNode.lastToken(viewMode: .sourceAccurate), tokens: closeTokens) + } else { + after(initializer.equal, tokens: .break(.continue)) + } + } + return .visitChildren } @@ -2275,12 +2295,13 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: InitializerClauseSyntax) -> SyntaxVisitorContinueKind { before(node.equal, tokens: .space) - // InitializerClauses that are children of a PatternBindingSyntax or + // InitializerClauses that are children of a PatternBindingSyntax, EnumCaseElementSyntax, or // OptionalBindingConditionSyntax are already handled in the latter node, to ensure that // continuations stack appropriately. if let parent = node.parent, !parent.is(PatternBindingSyntax.self) && !parent.is(OptionalBindingConditionSyntax.self) + && !parent.is(EnumCaseElementSyntax.self) { after(node.equal, tokens: .break) } diff --git a/Tests/SwiftFormatPrettyPrintTests/StringTests.swift b/Tests/SwiftFormatPrettyPrintTests/StringTests.swift index d82aba30c..a9cc75f96 100644 --- a/Tests/SwiftFormatPrettyPrintTests/StringTests.swift +++ b/Tests/SwiftFormatPrettyPrintTests/StringTests.swift @@ -419,6 +419,17 @@ final class StringTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: input + "\n", linelength: 100) } + func testMultilineStringsAsEnumRawValues() { + let input = #""" + enum E: String { + case x = """ + blah blah + """ + } + """# + assertPrettyPrintEqual(input: input, expected: input + "\n", linelength: 100) + } + func testMultilineStringsNestedInAnotherWrappingContext() { let input = #""" From e78eeb388746b5ee513ed79e6c37a5325249bb5a Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Fri, 7 Apr 2023 10:50:06 -0700 Subject: [PATCH 25/26] Prepare 508.0.0 release. --- README.md | 27 ++++++++++++++++------- Sources/swift-format/VersionOptions.swift | 2 +- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c66c94d64..f78af4826 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,25 @@ invoked via an [API](#api-usage). > and the code is provided so that it can be tested on real-world code and > experiments can be made by modifying it. -## Matching swift-format to Your Swift Version (Swift 5.7 and earlier) +## Matching swift-format to Your Swift Version -> NOTE: `swift-format` on the `main` branch now uses a version of -> [SwiftSyntax](https://github.com/apple/swift-syntax) whose parser has been -> rewritten in Swift and no longer has dependencies on libraries in the -> Swift toolchain. This allows `swift-format` to be built, developed, and -> run using any version of Swift that can compile it, decoupling it from -> the version that supported a particular syntax. +### Swift 5.8 and later + +As of Swift 5.8, swift-format depends on the version of +[SwiftSyntax](https://github.com/apple/swift-syntax) whose parser has been +rewritten in Swift and no longer has dependencies on libraries in the +Swift toolchain. + +This change allows `swift-format` to be built, developed, and run using +any version of Swift that can compile it, decoupling it from the version +that supported a particular syntax. However, earlier versions of swift-format +will still not be able to recognize new syntax added in later versions of the +language and parser. + +Note also that the version numbering scheme has changed to match +SwiftSyntax; the 5.8 release of swift-format is `508.0.0`, not `0.50800.0`. + +### Swift 5.7 and earlier `swift-format` versions 0.50700.0 and earlier depend on versions of [SwiftSyntax](https://github.com/apple/swift-syntax) that used a standalone @@ -54,7 +65,7 @@ then once you have identified the version you need, you can check out the source and build it using the following commands: ```sh -VERSION=0.50700.0 # replace this with the version you need +VERSION=508.0.0 # replace this with the version you need git clone https://github.com/apple/swift-format.git cd swift-format git checkout "tags/$VERSION" diff --git a/Sources/swift-format/VersionOptions.swift b/Sources/swift-format/VersionOptions.swift index 8ac851fdc..84a420181 100644 --- a/Sources/swift-format/VersionOptions.swift +++ b/Sources/swift-format/VersionOptions.swift @@ -20,7 +20,7 @@ struct VersionOptions: ParsableArguments { func validate() throws { if version { // TODO: Automate updates to this somehow. - print("0.50500.0") + print("508.0.0") throw ExitCode.success } } From 7970ac30f4243142d73cb24e851b22745e0c79c9 Mon Sep 17 00:00:00 2001 From: Tony Allevato Date: Thu, 29 Jun 2023 11:31:37 -0400 Subject: [PATCH 26/26] Prepare 5.9 release. --- README.md | 5 +++-- Sources/swift-format/VersionOptions.swift | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f78af4826..06edcdde6 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,8 @@ will still not be able to recognize new syntax added in later versions of the language and parser. Note also that the version numbering scheme has changed to match -SwiftSyntax; the 5.8 release of swift-format is `508.0.0`, not `0.50800.0`. +SwiftSyntax; the 5.8 release of swift-format is `508.0.0`, not `0.50800.0`, +and future versions are also expressed this way. ### Swift 5.7 and earlier @@ -65,7 +66,7 @@ then once you have identified the version you need, you can check out the source and build it using the following commands: ```sh -VERSION=508.0.0 # replace this with the version you need +VERSION=509.0.0 # replace this with the version you need git clone https://github.com/apple/swift-format.git cd swift-format git checkout "tags/$VERSION" diff --git a/Sources/swift-format/VersionOptions.swift b/Sources/swift-format/VersionOptions.swift index 84a420181..f1e873212 100644 --- a/Sources/swift-format/VersionOptions.swift +++ b/Sources/swift-format/VersionOptions.swift @@ -20,7 +20,7 @@ struct VersionOptions: ParsableArguments { func validate() throws { if version { // TODO: Automate updates to this somehow. - print("508.0.0") + print("509.0.0") throw ExitCode.success } }