diff --git a/Package.swift b/Package.swift index d7912810e..5bdae8fe5 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"), ] ), @@ -214,20 +213,17 @@ 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" ), - .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/README.md b/README.md index c66c94d64..06edcdde6 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,26 @@ 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`, +and future versions are also expressed this way. + +### 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 +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=0.50700.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/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 + ) +} 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/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/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..8e98b722a 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( @@ -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( @@ -253,29 +299,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) + before(genericWhereClause.firstToken(viewMode: .sourceAccurate), tokens: .break(.same), .open) after(memberBlock.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 +333,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 +341,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 +355,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 +375,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 +413,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 +430,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,13 +451,28 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } } - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) arrangeParameterClause(node.indices, forcesBreakBeforeRightParen: true) 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( @@ -421,17 +482,28 @@ 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) + } + + /// 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 @@ -442,29 +514,13 @@ 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 } 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 } @@ -484,8 +540,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 +550,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 +587,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 +627,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 +647,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 +661,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 +669,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 +691,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 +717,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 +746,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,18 +769,28 @@ 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 + // 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(viewMode: .sourceAccurate) != node.lastToken(viewMode: .sourceAccurate) { + after(node.lastToken(viewMode: .sourceAccurate), tokens: afterLastTokenTokens) + } else { + before(node.nextToken(viewMode: .sourceAccurate), tokens: afterLastTokenTokens) + } return .visitChildren } @@ -739,7 +805,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 = @@ -748,7 +814,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) } } @@ -827,9 +893,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) } @@ -847,8 +913,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) } @@ -858,12 +924,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 } @@ -889,9 +955,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) } @@ -901,12 +967,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 } @@ -922,7 +988,6 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { override func visit(_ node: MemberAccessExprSyntax) -> SyntaxVisitorContinueKind { preVisitInsertingContextualBreaks(node) - return .visitChildren } @@ -930,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) @@ -948,10 +1022,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) } } } @@ -1039,7 +1113,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]() @@ -1067,13 +1141,13 @@ 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]) } } 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 @@ -1108,7 +1182,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) @@ -1137,28 +1211,21 @@ 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) } } - 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, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) before(node.inTok, tokens: .break(.same)) return .visitChildren } @@ -1170,15 +1237,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 } @@ -1188,8 +1255,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) } } @@ -1237,11 +1304,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 } @@ -1282,7 +1361,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, @@ -1292,13 +1371,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))) @@ -1307,13 +1386,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, @@ -1323,7 +1402,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 } @@ -1335,7 +1414,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) } @@ -1343,8 +1422,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 } @@ -1373,30 +1452,24 @@ 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) } - if isNestedInPostfixIfConfig(node: Syntax(node)) { + if !isNestedInPostfixIfConfig(node: Syntax(node)), let condition = node.condition { before( - node.firstToken, - tokens: [ - .printerControl(kind: .enableBreaking), - .break(.reset), - ] - ) - } else if let condition = node.condition { - before(condition.firstToken, tokens: .printerControl(kind: .disableBreaking)) + 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)) } @@ -1411,11 +1484,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 } @@ -1438,12 +1511,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 } @@ -1455,7 +1528,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 } @@ -1471,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 } @@ -1489,13 +1582,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 } @@ -1506,7 +1599,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 } @@ -1518,11 +1611,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 } @@ -1537,8 +1630,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 } @@ -1566,7 +1659,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))) @@ -1575,7 +1668,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 } @@ -1583,8 +1676,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 } @@ -1602,7 +1693,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. @@ -1616,7 +1707,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. @@ -1649,12 +1740,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 } @@ -1664,7 +1755,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 { @@ -1684,7 +1775,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { case nil: break } - after(node.lastToken, tokens: .close) + after(node.lastToken(viewMode: .sourceAccurate), tokens: .close) return .visitChildren } @@ -1696,24 +1787,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 } @@ -1723,13 +1814,42 @@ 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(viewMode: .sourceAccurate), tokens: .printerControl(kind: .disableBreaking(allowDiscretionary: false))) + + arrangeAttributeList(node.attributes) after(node.importTok, tokens: .space) after(node.importKind, tokens: .space) + + after(node.lastToken(viewMode: .sourceAccurate), tokens: .printerControl(kind: .enableBreaking)) return .visitChildren } 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 } @@ -1749,9 +1869,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 @@ -1785,7 +1905,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 } @@ -1799,20 +1919,12 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } else { breakOrSpace = .break } - after(node.lastToken, tokens: breakOrSpace) + after(node.lastToken(viewMode: .sourceAccurate), tokens: breakOrSpace) return .visitChildren } 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, tokens: .break) + before(node.output?.firstToken(viewMode: .sourceAccurate), tokens: .break) return .visitChildren } @@ -1825,25 +1937,15 @@ 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?.previousToken { - 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; + // 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 binOp = node.operatorOperand let rhs = node.rightOperand maybeGroupAroundSubexpression(rhs, combiningOperator: binOp) @@ -1855,10 +1957,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) = stackedIndentationBehavior(after: binOp, rhs: rhs) + if let (unindentingNode, _, breakKind, shouldGroup) = + stackedIndentationBehavior(after: binOp, rhs: rhs) { beforeTokens = [.break(.open(kind: breakKind))] - after(unindentingNode.lastToken, tokens: [.break(.close(mustBreak: false), size: 0)]) + var afterTokens: [Token] = [.break(.close(mustBreak: false), size: 0)] + if shouldGroup { + beforeTokens.append(.open) + afterTokens.append(.close) + } + after( + unindentingNode.lastToken(viewMode: .sourceAccurate), + tokens: afterTokens) } else { beforeTokens = [.break(.continue)] } @@ -1866,13 +1976,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, tokens: .close) + after(rhs.lastToken(viewMode: .sourceAccurate), tokens: .close) } - after(binOp.lastToken, tokens: beforeTokens) - } else if let (unindentingNode, shouldReset, breakKind) = + after(binOp.lastToken(viewMode: .sourceAccurate), tokens: beforeTokens) + } else if let (unindentingNode, shouldReset, breakKind, shouldGroup) = stackedIndentationBehavior(after: binOp, rhs: rhs) { // For parenthesized expressions and for unparenthesized usages of `&&` and `||`, we don't @@ -1882,29 +1992,35 @@ 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, 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] = + var closeBreakTokens: [Token] = (shouldReset ? [.break(.reset, size: 0)] : []) - + [.break(.close(mustBreak: false), size: 0), .close] - after(unindentingNode.lastToken, tokens: closeBreakTokens) + + [.break(.close(mustBreak: false), size: 0)] + if shouldGroup { + closeBreakTokens.append(.close) + } + 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) } } @@ -1921,15 +2037,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 } @@ -1948,9 +2064,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 } @@ -1974,12 +2089,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 @@ -1998,25 +2113,33 @@ 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)) + 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)) } - 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) + if isCompoundExpression(expr) && leftmostMultilineStringLiteral(of: expr) == nil { + before(expr.firstToken(viewMode: .sourceAccurate), tokens: .open) + after(expr.lastToken(viewMode: .sourceAccurate), tokens: .close) } } @@ -2058,8 +2181,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 } @@ -2091,8 +2214,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 } @@ -2114,11 +2237,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 } @@ -2136,22 +2259,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 } @@ -2172,9 +2295,14 @@ 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, 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) } return .visitChildren @@ -2185,7 +2313,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 } @@ -2232,8 +2362,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 } @@ -2243,16 +2373,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 } @@ -2266,11 +2396,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 } @@ -2322,8 +2452,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 } @@ -2338,9 +2468,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 } @@ -2351,7 +2481,27 @@ 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)) + } + + 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 @@ -2375,10 +2525,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 } @@ -2560,13 +2710,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) } } @@ -2832,7 +2982,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 { @@ -3118,7 +3268,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 @@ -3147,7 +3297,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 @@ -3194,8 +3348,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 } @@ -3207,8 +3361,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) } } @@ -3269,36 +3423,73 @@ 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. + /// 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 leftmostExpr(of: asExpr.expression, ifMatching: predicate) + case .isExpr(let isExpr): + return leftmostExpr(of: isExpr.expression, ifMatching: predicate) + case .forcedValueExpr(let forcedValueExpr): + return leftmostExpr(of: forcedValueExpr.expression, ifMatching: predicate) + case .optionalChainingExpr(let optionalChainingExpr): + return leftmostExpr(of: optionalChainingExpr.expression, ifMatching: predicate) + case .postfixUnaryExpr(let postfixUnaryExpr): + return leftmostExpr(of: postfixUnaryExpr.expression, ifMatching: predicate) + case .prefixOperatorExpr(let prefixOperatorExpr): + 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 leftmostExpr(of: functionCallExpr.calledExpression, ifMatching: predicate) + case .subscriptExpr(let subscriptExpr): + return leftmostExpr(of: subscriptExpr.calledExpression, ifMatching: predicate) + case .memberAccessExpr(let memberAccessExpr): + return memberAccessExpr.base.flatMap { leftmostExpr(of: $0, ifMatching: predicate) } + case .postfixIfConfigExpr(let postfixIfConfigExpr): + 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. 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 { @@ -3308,8 +3499,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 @@ -3318,7 +3510,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 @@ -3338,9 +3530,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 + ) } } @@ -3350,8 +3551,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 @@ -3362,7 +3566,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: false + ) } if let innerExpr = parenthesizedExpr.elementList.first?.expression, @@ -3374,14 +3583,34 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } return ( - unindentingNode: Syntax(parenthesizedExpr), shouldReset: false, breakKind: .continuation) + unindentingNode: Syntax(parenthesizedExpr), + shouldReset: false, + breakKind: .continuation, + shouldGroup: false + ) } // 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 + ) + } + + 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. @@ -3435,7 +3664,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)) @@ -3444,7 +3673,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) @@ -3503,10 +3732,27 @@ 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 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) = @@ -3529,14 +3775,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) } @@ -3544,25 +3790,33 @@ 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) } } 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 } extension Syntax { @@ -3717,7 +3971,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 @@ -3729,7 +3983,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/NoAssignmentInExpressions.swift b/Sources/SwiftFormatRules/NoAssignmentInExpressions.swift index e327b59d7..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) && node.parent?.is(CodeBlockItemSyntax.self) == false { + if isAssignmentExpression(node) + && !isStandaloneAssignmentStatement(node) + && !isInAllowedFunction(node) + { diagnose(.moveAssignmentToOwnStatement, on: node) } return ExprSyntax(node) @@ -59,7 +63,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 +111,54 @@ 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) + } + + /// 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/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 162715254..b860b4590 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) @@ -46,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) } 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..42305eab9 --- /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) - } - } -} diff --git a/Sources/swift-format/VersionOptions.swift b/Sources/swift-format/VersionOptions.swift index 8ac851fdc..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("0.50500.0") + print("509.0.0") throw ExitCode.success } } 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/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) + } } 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) } diff --git a/Tests/SwiftFormatPrettyPrintTests/IfConfigTests.swift b/Tests/SwiftFormatPrettyPrintTests/IfConfigTests.swift index 9547816f0..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() { @@ -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() } """ @@ -299,6 +299,8 @@ final class IfConfigTests: PrettyPrintTestCase { #if os(iOS) || os(watchOS) #if os(iOS) .iOSModifier() + #elseif os(tvOS) + .tvOSModifier() #else .watchOSModifier() #endif @@ -311,14 +313,16 @@ 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() + #elseif os(tvOS) + .tvOSModifier() + #else + .watchOSModifier() + #endif + .iOSAndWatchOSModifier() #endif - .iOSAndWatchOSModifier() - #endif } """ @@ -326,7 +330,6 @@ final class IfConfigTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 45) } - func testPostfixPoundIfAfterVariables() { let input = """ @@ -343,10 +346,10 @@ final class IfConfigTests: PrettyPrintTestCase { """ VStack { textView - #if os(iOS) - .iOSSpecificModifier() - #endif - .commonModifier() + #if os(iOS) + .iOSSpecificModifier() + #endif + .commonModifier() } """ @@ -390,4 +393,127 @@ final class IfConfigTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 45) } + + func testPostfixPoundIfBetweenOtherModifiers() { + let input = + """ + EmptyView() + .padding([.vertical]) + #if os(iOS) + .iOSSpecificModifier() + .anotherIOSSpecificModifier() + #endif + .commonModifier() + """ + + let expected = + """ + EmptyView() + .padding([.vertical]) + #if os(iOS) + .iOSSpecificModifier() + .anotherIOSSpecificModifier() + #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) + } + + 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) + } } 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/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 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) } } 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) + } +} 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) + } +} diff --git a/Tests/SwiftFormatPrettyPrintTests/StringTests.swift b/Tests/SwiftFormatPrettyPrintTests/StringTests.swift index 13644ea0d..a9cc75f96 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,4 +350,150 @@ 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) + } + + func testMultilineStringsAsEnumRawValues() { + let input = #""" + enum E: String { + case x = """ + blah blah + """ + } + """# + 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 = + ##""" + 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) + } } diff --git a/Tests/SwiftFormatPrettyPrintTests/SwitchStmtTests.swift b/Tests/SwiftFormatPrettyPrintTests/SwitchStmtTests.swift index 35726d224..c2b744a1c 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 = """ @@ -260,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() { 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) - } -} diff --git a/Tests/SwiftFormatRulesTests/NoAssignmentInExpressionsTests.swift b/Tests/SwiftFormatRulesTests/NoAssignmentInExpressionsTests.swift index 238c2f92f..6ba554a0f 100644 --- a/Tests/SwiftFormatRulesTests/NoAssignmentInExpressionsTests.swift +++ b/Tests/SwiftFormatRulesTests/NoAssignmentInExpressionsTests.swift @@ -161,4 +161,54 @@ 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) + } + + 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) + } } 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 }) {} + """) + } }