diff --git a/CMakeLists.txt b/CMakeLists.txt index 00a9f061b..28bd4e530 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,7 +68,7 @@ endif() find_package(SwiftSyntax CONFIG) if(NOT SwiftSyntax_FOUND) FetchContent_Declare(Syntax - GIT_REPOSITORY https://github.com/apple/swift-syntax + GIT_REPOSITORY https://github.com/swiftlang/swift-syntax GIT_TAG main) list(APPEND _SF_VENDOR_DEPENDENCIES Syntax) endif() diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 2b0a60355..000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,55 +0,0 @@ -# Code of Conduct -To be a truly great community, Swift.org needs to welcome developers from all walks of life, -with different backgrounds, and with a wide range of experience. A diverse and friendly -community will have more great ideas, more unique perspectives, and produce more great -code. We will work diligently to make the Swift community welcoming to everyone. - -To give clarity of what is expected of our members, Swift.org has adopted the code of conduct -defined by [contributor-covenant.org](https://www.contributor-covenant.org). This document is used across many open source -communities, and we think it articulates our values well. The full text is copied below: - -### Contributor Code of Conduct v1.3 -As contributors and maintainers of this project, and in the interest of fostering an open and -welcoming community, we pledge to respect all people who contribute through reporting -issues, posting feature requests, updating documentation, submitting pull requests or patches, -and other activities. - -We are committed to making participation in this project a harassment-free experience for -everyone, regardless of level of experience, gender, gender identity and expression, sexual -orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or -nationality. - -Examples of unacceptable behavior by participants include: -- The use of sexualized language or imagery -- Personal attacks -- Trolling or insulting/derogatory comments -- Public or private harassment -- Publishing other’s private information, such as physical or electronic addresses, without explicit permission -- Other unethical or unprofessional conduct - -Project maintainers have the right and responsibility to remove, edit, or reject comments, -commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of -Conduct, or to ban temporarily or permanently any contributor for other behaviors that they -deem inappropriate, threatening, offensive, or harmful. - -By adopting this Code of Conduct, project maintainers commit themselves to fairly and -consistently applying these principles to every aspect of managing this project. Project -maintainers who do not follow or enforce the Code of Conduct may be permanently removed -from the project team. - -This code of conduct applies both within project spaces and in public spaces when an -individual is representing the project or its community. - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by -contacting a project maintainer at [conduct@swift.org](mailto:conduct@swift.org). All complaints will be reviewed and -investigated and will result in a response that is deemed necessary and appropriate to the -circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter -of an incident. - -*This policy is adapted from the Contributor Code of Conduct [version 1.3.0](http://contributor-covenant.org/version/1/3/0/).* - -### Reporting -A working group of community members is committed to promptly addressing any [reported -issues](mailto:conduct@swift.org). Working group members are volunteers appointed by the project lead, with a -preference for individuals with varied backgrounds and perspectives. Membership is expected -to change regularly, and may grow or shrink. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 9f01e1f3b..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,11 +0,0 @@ -By submitting a pull request, you represent that you have the right to license -your contribution to Apple and the community, and agree by submitting the patch -that your contributions are licensed under the [Swift -license](https://swift.org/LICENSE.txt). - ---- - -Before submitting the pull request, please make sure you have [tested your -changes](https://github.com/apple/swift/blob/main/docs/ContinuousIntegration.md) -and that they follow the Swift project [guidelines for contributing -code](https://swift.org/contributing/#contributing-code). diff --git a/Documentation/PrettyPrinter.md b/Documentation/PrettyPrinter.md index bfc81aacc..2883d65a5 100644 --- a/Documentation/PrettyPrinter.md +++ b/Documentation/PrettyPrinter.md @@ -263,7 +263,7 @@ if someCondition { ### Token Generation Token generation begins with the abstract syntax tree (AST) of the Swift source -file, provided by the [SwiftSyntax](https://github.com/apple/swift-syntax) +file, provided by the [SwiftSyntax](https://github.com/swiftlang/swift-syntax) library. We have overloaded a `visit` method for each of the different kinds of syntax nodes. Most of these nodes are higher-level, and are composed of other nodes. For example, `FunctionDeclSyntax` contains diff --git a/Package.swift b/Package.swift index 588f9b1d6..1067d33fd 100644 --- a/Package.swift +++ b/Package.swift @@ -163,7 +163,7 @@ var dependencies: [Package.Dependency] { return [ .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.2"), .package(url: "https://github.com/kkebo/swift-markdown.git", branch: "swift-markdown-wasm32-wasi-0.4"), - .package(url: "https://github.com/apple/swift-syntax.git", branch: "main"), + .package(url: "https://github.com/swiftlang/swift-syntax.git", branch: "main"), ] } } diff --git a/README.md b/README.md index d4792a834..14c3ecba2 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ The checked items are currently supported. The others are planned to be supporte # swift-format `swift-format` provides the formatting technology for -[SourceKit-LSP](https://github.com/apple/sourcekit-lsp) and the building +[SourceKit-LSP](https://github.com/swiftlang/sourcekit-lsp) and the building blocks for doing code formatting transformations. This package can be used as a [command line tool](#command-line-usage) @@ -44,7 +44,7 @@ invoked via an [API](#api-usage). ### 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 +[SwiftSyntax](https://github.com/swiftlang/swift-syntax) whose parser has been rewritten in Swift and no longer has dependencies on libraries in the Swift toolchain. @@ -60,7 +60,7 @@ SwiftSyntax; the 5.8 release of swift-format is `508.0.0`, not `0.50800.0`. ### Swift 5.7 and earlier `swift-format` versions 0.50700.0 and earlier depend on versions of -[SwiftSyntax](https://github.com/apple/swift-syntax) that used a standalone +[SwiftSyntax](https://github.com/swiftlang/swift-syntax) that used a standalone parsing library distributed as part of the Swift toolchain. When using these versions, you should check out and build `swift-format` from the release tag or branch that is compatible with the version of Swift you are using. @@ -87,8 +87,11 @@ For example, if you are using Xcode 13.3 (Swift 5.6), you will need ## Getting swift-format If you are mainly interested in using swift-format (rather than developing it), -then you can get swift-format either via [Homebrew](https://brew.sh/) or by checking out the -source and building it. +then you can get it in three different ways: + +### Included in Xcode + +Xcode 16 and above include swift-format in the toolchain. You can run `swift-format` from anywhere on the system using `swift format` (notice the space instead of dash). To find the path at which `swift-format` is installed, run `xcrun --find swift-format`. ### Installing via Homebrew @@ -100,7 +103,7 @@ Install `swift-format` using the following commands: ```sh VERSION=510.1.0 # replace this with the version you need -git clone https://github.com/apple/swift-format.git +git clone https://github.com/swiftlang/swift-format.git cd swift-format git checkout "tags/$VERSION" swift build -c release @@ -277,3 +280,24 @@ been merged into `main`. If you are interested in developing `swift-format`, there is additional documentation about that [here](Documentation/Development.md). + +## Contributing + +Contributions to Swift are welcomed and encouraged! Please see the +[Contributing to Swift guide](https://swift.org/contributing/). + +Before submitting the pull request, please make sure you have [tested your + changes](https://github.com/apple/swift/blob/main/docs/ContinuousIntegration.md) + and that they follow the Swift project [guidelines for contributing + code](https://swift.org/contributing/#contributing-code). + +To be a truly great community, [Swift.org](https://swift.org/) needs to welcome +developers from all walks of life, with different backgrounds, and with a wide +range of experience. A diverse and friendly community will have more great +ideas, more unique perspectives, and produce more great code. We will work +diligently to make the Swift community welcoming to everyone. + +To give clarity of what is expected of our members, Swift has adopted the +code of conduct defined by the Contributor Covenant. This document is used +across many open source communities, and we think it articulates our values +well. For more, see the [Code of Conduct](https://swift.org/code-of-conduct/). diff --git a/Sources/SwiftFormat/API/Selection.swift b/Sources/SwiftFormat/API/Selection.swift new file mode 100644 index 000000000..9ea599db3 --- /dev/null +++ b/Sources/SwiftFormat/API/Selection.swift @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +import SwiftSyntax + +/// The selection as given on the command line - an array of offets and lengths +public enum Selection { + case infinite + case ranges([Range]) + + /// Create a selection from an array of utf8 ranges. An empty array means an infinite selection. + public init(offsetRanges: [Range]) { + if offsetRanges.isEmpty { + self = .infinite + } else { + let ranges = offsetRanges.map { + AbsolutePosition(utf8Offset: $0.lowerBound) ..< AbsolutePosition(utf8Offset: $0.upperBound) + } + self = .ranges(ranges) + } + } + + public func contains(_ position: AbsolutePosition) -> Bool { + switch self { + case .infinite: + return true + case .ranges(let ranges): + return ranges.contains { $0.contains(position) } + } + } + + public func overlapsOrTouches(_ range: Range) -> Bool { + switch self { + case .infinite: + return true + case .ranges(let ranges): + return ranges.contains { $0.overlapsOrTouches(range) } + } + } +} + + +public extension Syntax { + /// - Returns: `true` if the node is _completely_ inside any range in the selection + func isInsideSelection(_ selection: Selection) -> Bool { + switch selection { + case .infinite: + return true + case .ranges(let ranges): + return ranges.contains { return $0.lowerBound <= position && endPosition <= $0.upperBound } + } + } +} diff --git a/Sources/SwiftFormat/API/SwiftFormatter.swift b/Sources/SwiftFormat/API/SwiftFormatter.swift index 9230bdd8f..e91030b3c 100644 --- a/Sources/SwiftFormat/API/SwiftFormatter.swift +++ b/Sources/SwiftFormat/API/SwiftFormatter.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 @@ -70,6 +70,7 @@ public final class SwiftFormatter { try format( source: String(contentsOf: url, encoding: .utf8), assumingFileURL: url, + selection: .infinite, to: &outputStream, parsingDiagnosticHandler: parsingDiagnosticHandler) } @@ -86,6 +87,7 @@ public final class SwiftFormatter { /// - url: A file URL denoting the filename/path that should be assumed for this syntax tree, /// which is associated with any diagnostics emitted during formatting. If this is nil, a /// dummy value will be used. + /// - selection: The ranges to format /// - outputStream: A value conforming to `TextOutputStream` to which the formatted output will /// be written. /// - parsingDiagnosticHandler: An optional callback that will be notified if there are any @@ -94,6 +96,7 @@ public final class SwiftFormatter { public func format( source: String, assumingFileURL url: URL?, + selection: Selection, to outputStream: inout Output, parsingDiagnosticHandler: ((Diagnostic, SourceLocation) -> Void)? = nil ) throws { @@ -108,8 +111,8 @@ public final class SwiftFormatter { assumingFileURL: url, parsingDiagnosticHandler: parsingDiagnosticHandler) try format( - syntax: sourceFile, operatorTable: .standardOperators, assumingFileURL: url, source: source, - to: &outputStream) + syntax: sourceFile, source: source, operatorTable: .standardOperators, assumingFileURL: url, + selection: selection, to: &outputStream) } /// Formats the given Swift syntax tree and writes the result to an output stream. @@ -122,32 +125,26 @@ public final class SwiftFormatter { /// /// - Parameters: /// - syntax: The Swift syntax tree to be converted to source code and formatted. + /// - source: The original Swift source code used to build the syntax tree. /// - operatorTable: The table that defines the operators and their precedence relationships. /// This must be the same operator table that was used to fold the expressions in the `syntax` /// argument. /// - url: A file URL denoting the filename/path that should be assumed for this syntax tree, /// which is associated with any diagnostics emitted during formatting. If this is nil, a /// dummy value will be used. + /// - selection: The ranges to format /// - outputStream: A value conforming to `TextOutputStream` to which the formatted output will /// be written. /// - Throws: If an unrecoverable error occurs when formatting the code. public func format( - syntax: SourceFileSyntax, operatorTable: OperatorTable, assumingFileURL url: URL?, - to outputStream: inout Output - ) throws { - try format( - syntax: syntax, operatorTable: operatorTable, assumingFileURL: url, source: nil, - to: &outputStream) - } - - private func format( - syntax: SourceFileSyntax, operatorTable: OperatorTable, - assumingFileURL url: URL?, source: String?, to outputStream: inout Output + syntax: SourceFileSyntax, source: String, operatorTable: OperatorTable, + assumingFileURL url: URL?, selection: Selection, to outputStream: inout Output ) throws { let assumedURL = url ?? URL(fileURLWithPath: "source") let context = Context( configuration: configuration, operatorTable: operatorTable, findingConsumer: findingConsumer, - fileURL: assumedURL, sourceFileSyntax: syntax, source: source, ruleNameCache: ruleNameCache) + fileURL: assumedURL, selection: selection, sourceFileSyntax: syntax, source: source, + ruleNameCache: ruleNameCache) let pipeline = FormatPipeline(context: context) let transformedSyntax = pipeline.rewrite(Syntax(syntax)) @@ -158,6 +155,7 @@ public final class SwiftFormatter { let printer = PrettyPrinter( context: context, + source: source, node: transformedSyntax, printTokenStream: debugOptions.contains(.dumpTokenStream), whitespaceOnly: false) diff --git a/Sources/SwiftFormat/API/SwiftLinter.swift b/Sources/SwiftFormat/API/SwiftLinter.swift index 4806f19df..79568e2cb 100644 --- a/Sources/SwiftFormat/API/SwiftLinter.swift +++ b/Sources/SwiftFormat/API/SwiftLinter.swift @@ -119,17 +119,18 @@ public final class SwiftLinter { /// - Throws: If an unrecoverable error occurs when formatting the code. public func lint( syntax: SourceFileSyntax, + source: String, operatorTable: OperatorTable, assumingFileURL url: URL ) throws { - try lint(syntax: syntax, operatorTable: operatorTable, assumingFileURL: url, source: nil) + try lint(syntax: syntax, operatorTable: operatorTable, assumingFileURL: url, source: source) } private func lint( syntax: SourceFileSyntax, operatorTable: OperatorTable, assumingFileURL url: URL, - source: String? + source: String ) throws { let context = Context( configuration: configuration, operatorTable: operatorTable, findingConsumer: findingConsumer, @@ -145,6 +146,7 @@ public final class SwiftLinter { // pretty-printer. let printer = PrettyPrinter( context: context, + source: source, node: Syntax(syntax), printTokenStream: debugOptions.contains(.dumpTokenStream), whitespaceOnly: true) diff --git a/Sources/SwiftFormat/CMakeLists.txt b/Sources/SwiftFormat/CMakeLists.txt index 4ce86a341..cb3998722 100644 --- a/Sources/SwiftFormat/CMakeLists.txt +++ b/Sources/SwiftFormat/CMakeLists.txt @@ -14,6 +14,7 @@ add_library(SwiftFormat API/Finding.swift API/FindingCategorizing.swift API/Indent.swift + API/Selection.swift API/SwiftFormatError.swift API/SwiftFormatter.swift API/SwiftLinter.swift diff --git a/Sources/SwiftFormat/Core/Context.swift b/Sources/SwiftFormat/Core/Context.swift index 29e69b0dc..2bbb900ac 100644 --- a/Sources/SwiftFormat/Core/Context.swift +++ b/Sources/SwiftFormat/Core/Context.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 @@ -39,6 +39,9 @@ public final class Context { /// The configuration for this run of the pipeline, provided by a configuration JSON file. let configuration: Configuration + /// The selection to process + let selection: Selection + /// Defines the operators and their precedence relationships that were used during parsing. let operatorTable: OperatorTable @@ -66,6 +69,7 @@ public final class Context { operatorTable: OperatorTable, findingConsumer: ((Finding) -> Void)?, fileURL: URL, + selection: Selection = .infinite, sourceFileSyntax: SourceFileSyntax, source: String? = nil, ruleNameCache: [ObjectIdentifier: String] @@ -74,6 +78,7 @@ public final class Context { self.operatorTable = operatorTable self.findingEmitter = FindingEmitter(consumer: findingConsumer) self.fileURL = fileURL + self.selection = selection self.importsXCTest = .notDetermined let tree = source.map { Parser.parse(source: $0) } ?? sourceFileSyntax self.sourceLocationConverter = @@ -86,8 +91,10 @@ public final class Context { } /// Given a rule's name and the node it is examining, determine if the rule is disabled at this - /// location or not. - func isRuleEnabled(_ rule: R.Type, node: Syntax) -> Bool { + /// location or not. Also makes sure the entire node is contained inside any selection. + func shouldFormat(_ rule: R.Type, node: Syntax) -> Bool { + guard node.isInsideSelection(selection) else { return false } + let loc = node.startLocation(converter: self.sourceLocationConverter) assert( diff --git a/Sources/SwiftFormat/Core/DocumentationCommentText.swift b/Sources/SwiftFormat/Core/DocumentationCommentText.swift index 44ef1a61a..32dd82a18 100644 --- a/Sources/SwiftFormat/Core/DocumentationCommentText.swift +++ b/Sources/SwiftFormat/Core/DocumentationCommentText.swift @@ -67,24 +67,7 @@ public struct DocumentationCommentText { // comment. We have to copy it into an array since `Trivia` doesn't support bidirectional // indexing. let triviaArray = Array(trivia) - let commentStartIndex: Array.Index - if - let lastNonDocCommentIndex = triviaArray.lastIndex(where: { - switch $0 { - case .docBlockComment, .docLineComment, - .newlines(1), .carriageReturns(1), .carriageReturnLineFeeds(1), - .spaces, .tabs: - return false - default: - return true - } - }), - lastNonDocCommentIndex != trivia.endIndex - { - commentStartIndex = triviaArray.index(after: lastNonDocCommentIndex) - } else { - commentStartIndex = triviaArray.startIndex - } + let commentStartIndex = findCommentStartIndex(triviaArray) // Determine the indentation level of the first line of the comment. This is used to adjust // block comments, whose text spans multiple lines. @@ -216,3 +199,37 @@ private func asciiArtLength(of string: Substring, leadingSpaces: Int) -> Int { } return 0 } + +/// Returns the start index of the earliest comment in the Trivia if we work backwards and +/// skip through comments, newlines, and whitespace. Then we advance a bit forward to be sure +/// the returned index is actually a comment and not whitespace. +private func findCommentStartIndex(_ triviaArray: Array) -> Array.Index { + func firstCommentIndex(_ slice: ArraySlice) -> Array.Index { + return slice.firstIndex(where: { + switch $0 { + case .docLineComment, .docBlockComment: + return true + default: + return false + } + }) ?? slice.endIndex + } + + if + let lastNonDocCommentIndex = triviaArray.lastIndex(where: { + switch $0 { + case .docBlockComment, .docLineComment, + .newlines(1), .carriageReturns(1), .carriageReturnLineFeeds(1), + .spaces, .tabs: + return false + default: + return true + } + }) + { + let nextIndex = triviaArray.index(after: lastNonDocCommentIndex) + return firstCommentIndex(triviaArray[nextIndex...]) + } else { + return firstCommentIndex(triviaArray[...]) + } +} diff --git a/Sources/SwiftFormat/Core/LintPipeline.swift b/Sources/SwiftFormat/Core/LintPipeline.swift index 3eb10072d..58d9f6d13 100644 --- a/Sources/SwiftFormat/Core/LintPipeline.swift +++ b/Sources/SwiftFormat/Core/LintPipeline.swift @@ -28,7 +28,7 @@ extension LintPipeline { func visitIfEnabled( _ visitor: (Rule) -> (Node) -> SyntaxVisitorContinueKind, for node: Node ) { - guard context.isRuleEnabled(Rule.self, node: Syntax(node)) else { return } + guard context.shouldFormat(Rule.self, node: Syntax(node)) else { return } let ruleId = ObjectIdentifier(Rule.self) guard self.shouldSkipChildren[ruleId] == nil else { return } let rule = self.rule(Rule.self) @@ -54,7 +54,7 @@ extension LintPipeline { // more importantly because the `visit` methods return protocol refinements of `Syntax` that // cannot currently be expressed as constraints without duplicating this function for each of // them individually. - guard context.isRuleEnabled(Rule.self, node: Syntax(node)) else { return } + guard context.shouldFormat(Rule.self, node: Syntax(node)) else { return } guard self.shouldSkipChildren[ObjectIdentifier(Rule.self)] == nil else { return } let rule = self.rule(Rule.self) _ = visitor(rule)(node) diff --git a/Sources/SwiftFormat/Core/SyntaxFormatRule.swift b/Sources/SwiftFormat/Core/SyntaxFormatRule.swift index 767e59fcf..92fc7c835 100644 --- a/Sources/SwiftFormat/Core/SyntaxFormatRule.swift +++ b/Sources/SwiftFormat/Core/SyntaxFormatRule.swift @@ -32,7 +32,7 @@ public class SyntaxFormatRule: SyntaxRewriter, Rule { public override func visitAny(_ node: Syntax) -> Syntax? { // If the rule is not enabled, then return the node unmodified; otherwise, returning nil tells // SwiftSyntax to continue with the standard dispatch. - guard context.isRuleEnabled(type(of: self), node: node) else { return node } + guard context.shouldFormat(type(of: self), node: node) else { return node } return nil } } diff --git a/Sources/SwiftFormat/PrettyPrint/Indent+Length.swift b/Sources/SwiftFormat/PrettyPrint/Indent+Length.swift index 062d50a07..3094735be 100644 --- a/Sources/SwiftFormat/PrettyPrint/Indent+Length.swift +++ b/Sources/SwiftFormat/PrettyPrint/Indent+Length.swift @@ -22,10 +22,10 @@ extension Indent { return String(repeating: character, count: count) } - func length(in configuration: Configuration) -> Int { + func length(tabWidth: Int) -> Int { switch self { case .spaces(let count): return count - case .tabs(let count): return count * configuration.tabWidth + case .tabs(let count): return count * tabWidth } } } @@ -36,6 +36,10 @@ extension Array where Element == Indent { } func length(in configuration: Configuration) -> Int { - return reduce(into: 0) { $0 += $1.length(in: configuration) } + return self.length(tabWidth: configuration.tabWidth) + } + + func length(tabWidth: Int) -> Int { + return reduce(into: 0) { $0 += $1.length(tabWidth: tabWidth) } } } diff --git a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift index 1201f84c2..ada574f1c 100644 --- a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import SwiftSyntax +import Foundation /// PrettyPrinter takes a Syntax node and outputs a well-formatted, re-indented reproduction of the /// code as a String. @@ -66,10 +67,22 @@ public class PrettyPrinter { private var configuration: Configuration { return context.configuration } private let maxLineLength: Int private var tokens: [Token] - private var outputBuffer: String = "" + private var source: String - /// The number of spaces remaining on the current line. - private var spaceRemaining: Int + /// Keep track of where formatting was disabled in the original source + /// + /// To format a selection, we insert `enableFormatting`/`disableFormatting` tokens into the + /// stream when entering/exiting a selection range. Those tokens include utf8 offsets into the + /// original source. When enabling formatting, we copy the text between `disabledPosition` and the + /// current position to `outputBuffer`. From then on, we continue to format until the next + /// `disableFormatting` token. + private var disabledPosition: AbsolutePosition? = nil { + didSet { + outputBuffer.isEnabled = disabledPosition == nil + } + } + + private var outputBuffer: PrettyPrintBuffer /// Keep track of the token lengths. private var lengths = [Int]() @@ -91,7 +104,11 @@ public class PrettyPrinter { /// Keeps track of the line numbers and indentation states of the open (and unclosed) breaks seen /// so far. - private var activeOpenBreaks: [ActiveOpenBreak] = [] + private var activeOpenBreaks: [ActiveOpenBreak] = [] { + didSet { + outputBuffer.currentIndentation = currentIndentation + } + } /// Stack of the active breaking contexts. private var activeBreakingContexts: [ActiveBreakingContext] = [] @@ -99,11 +116,14 @@ public class PrettyPrinter { /// The most recently ended breaking context, used to force certain following `contextual` breaks. private var lastEndedBreakingContext: ActiveBreakingContext? = nil - /// Keeps track of the current line number being printed. - private var lineNumber: Int = 1 - /// Indicates whether or not the current line being printed is a continuation line. - private var currentLineIsContinuation = false + private var currentLineIsContinuation = false { + didSet { + if oldValue != currentLineIsContinuation { + outputBuffer.currentIndentation = currentIndentation + } + } + } /// Keeps track of the continuation line state as you go into and out of open-close break groups. private var continuationStack: [Bool] = [] @@ -112,18 +132,6 @@ public class PrettyPrinter { /// corresponding end token are encountered. private var commaDelimitedRegionStack: [Int] = [] - /// Keeps track of the most recent number of consecutive newlines that have been printed. - /// - /// This value is reset to zero whenever non-newline content is printed. - private var consecutiveNewlineCount = 0 - - /// Keeps track of the most recent number of spaces that should be printed before the next text - /// token. - private var pendingSpaces = 0 - - /// Indicates whether or not the printer is currently at the beginning of a line. - private var isAtStartOfLine = true - /// Tracks how many printer control tokens to suppress firing breaks are active. private var activeBreakSuppressionCount = 0 @@ -161,7 +169,7 @@ public class PrettyPrinter { /// line number to increase by one by the time we reach the break, when we really wish to consider /// the break as being located at the end of the previous line. private var openCloseBreakCompensatingLineNumber: Int { - return isAtStartOfLine ? lineNumber - 1 : lineNumber + return outputBuffer.lineNumber - (outputBuffer.isAtStartOfLine ? 1 : 0) } /// Creates a new PrettyPrinter with the provided formatting configuration. @@ -172,81 +180,18 @@ public class PrettyPrinter { /// - printTokenStream: Indicates whether debug information about the token stream should be /// printed to standard output. /// - whitespaceOnly: Whether only whitespace changes should be made. - public init(context: Context, node: Syntax, printTokenStream: Bool, whitespaceOnly: Bool) { + public init(context: Context, source: String, node: Syntax, printTokenStream: Bool, whitespaceOnly: Bool) { self.context = context + self.source = source let configuration = context.configuration self.tokens = node.makeTokenStream( - configuration: configuration, operatorTable: context.operatorTable) + configuration: configuration, + selection: context.selection, + operatorTable: context.operatorTable) self.maxLineLength = configuration.lineLength - self.spaceRemaining = self.maxLineLength self.printTokenStream = printTokenStream self.whitespaceOnly = whitespaceOnly - } - - /// Append the given string to the output buffer. - /// - /// No further processing is performed on the string. - private func writeRaw(_ str: S) { - outputBuffer.append(String(str)) - } - - /// Writes newlines into the output stream, taking into account any preexisting consecutive - /// newlines and the maximum allowed number of blank lines. - /// - /// This function does some implicit collapsing of consecutive newlines to ensure that the - /// results are consistent when breaks and explicit newlines coincide. For example, imagine a - /// break token that fires (thus creating a single non-discretionary newline) because it is - /// followed by a group that contains 2 discretionary newlines that were found in the user's - /// source code at that location. In that case, the break "overlaps" with the discretionary - /// newlines and it will write a newline before we get to the discretionaries. Thus, we have to - /// subtract the previously written newlines during the second call so that we end up with the - /// correct number overall. - /// - /// - Parameter newlines: The number and type of newlines to write. - private func writeNewlines(_ newlines: NewlineBehavior) { - let numberToPrint: Int - switch newlines { - case .elective: - numberToPrint = consecutiveNewlineCount == 0 ? 1 : 0 - case .soft(let count, _): - // We add 1 to the max blank lines because it takes 2 newlines to create the first blank line. - numberToPrint = min(count, configuration.maximumBlankLines + 1) - consecutiveNewlineCount - case .hard(let count): - numberToPrint = count - } - - guard numberToPrint > 0 else { return } - writeRaw(String(repeating: "\n", count: numberToPrint)) - lineNumber += numberToPrint - isAtStartOfLine = true - consecutiveNewlineCount += numberToPrint - pendingSpaces = 0 - } - - /// Request that the given number of spaces be printed out before the next text token. - /// - /// Spaces are printed only when the next text token is printed in order to prevent us from - /// printing lines that are only whitespace or have trailing whitespace. - private func enqueueSpaces(_ count: Int) { - pendingSpaces += count - spaceRemaining -= count - } - - /// Writes the given text to the output stream. - /// - /// Before printing the text, this function will print any line-leading indentation or interior - /// leading spaces that are required before the text itself. - private func write(_ text: String) { - if isAtStartOfLine { - writeRaw(currentIndentation.indentation()) - spaceRemaining = maxLineLength - currentIndentation.length(in: configuration) - isAtStartOfLine = false - } else if pendingSpaces > 0 { - writeRaw(String(repeating: " ", count: pendingSpaces)) - } - writeRaw(text) - consecutiveNewlineCount = 0 - pendingSpaces = 0 + self.outputBuffer = PrettyPrintBuffer(maximumBlankLines: configuration.maximumBlankLines, tabWidth: configuration.tabWidth) } /// Print out the provided token, and apply line-wrapping and indentation as needed. @@ -268,7 +213,7 @@ public class PrettyPrinter { switch token { case .contextualBreakingStart: - activeBreakingContexts.append(ActiveBreakingContext(lineNumber: lineNumber)) + activeBreakingContexts.append(ActiveBreakingContext(lineNumber: outputBuffer.lineNumber)) // Discard the last finished breaking context to keep it from effecting breaks inside of the // new context. The discarded context has already either had an impact on the contextual break @@ -289,7 +234,7 @@ public class PrettyPrinter { // the group. case .open(let breaktype): // Determine if the break tokens in this group need to be forced. - if (length > spaceRemaining || lastBreak), case .consistent = breaktype { + if (!canFit(length) || lastBreak), case .consistent = breaktype { forceBreakStack.append(true) } else { forceBreakStack.append(false) @@ -331,7 +276,7 @@ public class PrettyPrinter { // scope), so we need the continuation indentation to persist across all the lines in that // scope. Additionally, continuation open breaks must indent when the break fires. let continuationBreakWillFire = openKind == .continuation - && (isAtStartOfLine || length > spaceRemaining || mustBreak) + && (outputBuffer.isAtStartOfLine || !canFit(length) || mustBreak) let contributesContinuationIndent = currentLineIsContinuation || continuationBreakWillFire activeOpenBreaks.append( @@ -360,7 +305,7 @@ public class PrettyPrinter { if matchingOpenBreak.contributesBlockIndent { // The actual line number is used, instead of the compensating line number. When the close // break is at the start of a new line, the block indentation isn't carried to the new line. - let currentLine = lineNumber + let currentLine = outputBuffer.lineNumber // When two or more open breaks are encountered on the same line, only the final open // break is allowed to increase the block indent, avoiding multiple block indents. As the // open breaks on that line are closed, the new final open break must be enabled again to @@ -378,7 +323,7 @@ public class PrettyPrinter { // If it's a mandatory breaking close, then we must break (regardless of line length) if // the break is on a different line than its corresponding open break. mustBreak = openedOnDifferentLine - } else if spaceRemaining == 0 { + } else if !canFit() { // If there is no room left on the line, then we must force this break to fire so that the // next token that comes along (typically a closing bracket of some kind) ends up on the // next line. @@ -436,13 +381,13 @@ public class PrettyPrinter { // context includes a multiline trailing closure or multiline function argument list. if let lastBreakingContext = lastEndedBreakingContext { if configuration.lineBreakAroundMultilineExpressionChainComponents { - mustBreak = lastBreakingContext.lineNumber != lineNumber + mustBreak = lastBreakingContext.lineNumber != outputBuffer.lineNumber } } // Wait for a contextual break to fire and then update the breaking behavior for the rest of // the contextual breaks in this scope to match the behavior of the one that fired. - let willFire = (!isAtStartOfLine && length > spaceRemaining) || mustBreak + let willFire = !canFit(length) || mustBreak if willFire { // Update the active breaking context according to the most recently finished breaking // context so all following contextual breaks in this scope to have matching behavior. @@ -451,7 +396,7 @@ public class PrettyPrinter { case .unset = activeContext.contextualBreakingBehavior { activeBreakingContexts[activeBreakingContexts.count - 1].contextualBreakingBehavior = - (closedContext.lineNumber == lineNumber) ? .continuation : .maintain + (closedContext.lineNumber == outputBuffer.lineNumber) ? .continuation : .maintain } } @@ -482,12 +427,12 @@ public class PrettyPrinter { } let suppressBreaking = isBreakingSuppressed && !overrideBreakingSuppressed - if !suppressBreaking && ((!isAtStartOfLine && length > spaceRemaining) || mustBreak) { + if !suppressBreaking && (!canFit(length) || mustBreak) { currentLineIsContinuation = isContinuationIfBreakFires - writeNewlines(newline) + outputBuffer.writeNewlines(newline) lastBreak = true } else { - if isAtStartOfLine { + if outputBuffer.isAtStartOfLine { // Make sure that the continuation status is correct even at the beginning of a line // (for example, after a newline token). This is necessary because a discretionary newline // might be inserted into the token stream before a continuation break, and the length of @@ -495,39 +440,33 @@ public class PrettyPrinter { // treat the line as a continuation. currentLineIsContinuation = isContinuationIfBreakFires } - enqueueSpaces(size) + outputBuffer.enqueueSpaces(size) lastBreak = false } // Print out the number of spaces according to the size, and adjust spaceRemaining. case .space(let size, _): - enqueueSpaces(size) + outputBuffer.enqueueSpaces(size) // Print any indentation required, followed by the text content of the syntax token. case .syntax(let text): guard !text.isEmpty else { break } lastBreak = false - write(text) - spaceRemaining -= text.count + outputBuffer.write(text) case .comment(let comment, let wasEndOfLine): lastBreak = false - write(comment.print(indent: currentIndentation)) if wasEndOfLine { - if comment.length > spaceRemaining && !isBreakingSuppressed { + if !(canFit(comment.length) || isBreakingSuppressed) { diagnose(.moveEndOfLineComment, category: .endOfLineComment) } - } else { - spaceRemaining -= comment.length } + outputBuffer.write(comment.print(indent: currentIndentation)) case .verbatim(let verbatim): - writeRaw(verbatim.print(indent: currentIndentation)) - consecutiveNewlineCount = 0 - pendingSpaces = 0 + outputBuffer.writeVerbatim(verbatim.print(indent: currentIndentation), length) lastBreak = false - spaceRemaining -= length case .printerControl(let kind): switch kind { @@ -566,12 +505,44 @@ public class PrettyPrinter { let shouldWriteComma = whitespaceOnly ? hasTrailingComma : shouldHaveTrailingComma if shouldWriteComma { - write(",") - spaceRemaining -= 1 + outputBuffer.write(",") + } + + case .enableFormatting(let enabledPosition): + guard let disabledPosition else { + // if we're not disabled, we ignore the token + break + } + let start = source.utf8.index(source.utf8.startIndex, offsetBy: disabledPosition.utf8Offset) + let end: String.Index + if let enabledPosition { + end = source.utf8.index(source.utf8.startIndex, offsetBy: enabledPosition.utf8Offset) + } else { + end = source.endIndex } + var text = String(source[start.. Bool { + let spaceRemaining = configuration.lineLength - outputBuffer.column + return outputBuffer.isAtStartOfLine || length <= spaceRemaining + } + /// Scan over the array of Tokens and calculate their lengths. /// /// This method is based on the `scan` function described in Derek Oppen's "Pretty Printing" paper @@ -673,6 +644,10 @@ public class PrettyPrinter { let length = isSingleElement ? 0 : 1 total += length lengths.append(length) + + case .enableFormatting, .disableFormatting: + // no effect on length calculations + lengths.append(0) } } @@ -694,7 +669,7 @@ public class PrettyPrinter { fatalError("At least one .break(.open) was not matched by a .break(.close)") } - return outputBuffer + return outputBuffer.output } /// Used to track the indentation level for the debug token stream output. @@ -775,17 +750,25 @@ public class PrettyPrinter { case .contextualBreakingEnd: printDebugIndent() print("[END BREAKING CONTEXT Idx: \(idx)]") + + case .enableFormatting(let pos): + printDebugIndent() + print("[ENABLE FORMATTING utf8 offset: \(String(describing: pos))]") + + case .disableFormatting(let pos): + printDebugIndent() + print("[DISABLE FORMATTING utf8 offset: \(pos)]") } } /// Emits a finding with the given message and category at the current location in `outputBuffer`. private func diagnose(_ message: Finding.Message, category: PrettyPrintFindingCategory) { // Add 1 since columns uses 1-based indices. - let column = maxLineLength - spaceRemaining + 1 + let column = outputBuffer.column + 1 context.findingEmitter.emit( message, category: category, - location: Finding.Location(file: context.fileURL.path, line: lineNumber, column: column)) + location: Finding.Location(file: context.fileURL.path, line: outputBuffer.lineNumber, column: column)) } } diff --git a/Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift new file mode 100644 index 000000000..02a2a7ac6 --- /dev/null +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift @@ -0,0 +1,150 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 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 + +/// Used by the PrettyPrint class to actually assemble the output string. This struct +/// tracks state specific to the output (line number, column, etc.) rather than the pretty +/// printing algorithm itself. +struct PrettyPrintBuffer { + /// The maximum number of consecutive blank lines that may appear in a file. + let maximumBlankLines: Int + + /// The width of the horizontal tab in spaces. + let tabWidth: Int + + /// If true, output is generated as normal. If false, the various state variables are + /// updated as normal but nothing is appended to the output (used by selection formatting). + var isEnabled: Bool = true + + /// Indicates whether or not the printer is currently at the beginning of a line. + private(set) var isAtStartOfLine: Bool = true + + /// Keeps track of the most recent number of consecutive newlines that have been printed. + /// + /// This value is reset to zero whenever non-newline content is printed. + private(set) var consecutiveNewlineCount: Int = 0 + + /// Keeps track of the current line number being printed. + private(set) var lineNumber: Int = 1 + + /// Keeps track of the most recent number of spaces that should be printed before the next text + /// token. + private(set) var pendingSpaces: Int = 0 + + /// Current column position of the printer. If we just printed a newline and nothing else, it + /// will still point to the position of the previous line. + private(set) var column: Int + + /// The current indentation level to be used when text is appended to a new line. + var currentIndentation: [Indent] + + /// The accumulated output of the pretty printer. + private(set) var output: String = "" + + init(maximumBlankLines: Int, tabWidth: Int, column: Int = 0) { + self.maximumBlankLines = maximumBlankLines + self.tabWidth = tabWidth + self.currentIndentation = [] + self.column = column + } + + /// Writes newlines into the output stream, taking into account any preexisting consecutive + /// newlines and the maximum allowed number of blank lines. + /// + /// This function does some implicit collapsing of consecutive newlines to ensure that the + /// results are consistent when breaks and explicit newlines coincide. For example, imagine a + /// break token that fires (thus creating a single non-discretionary newline) because it is + /// followed by a group that contains 2 discretionary newlines that were found in the user's + /// source code at that location. In that case, the break "overlaps" with the discretionary + /// newlines and it will write a newline before we get to the discretionaries. Thus, we have to + /// subtract the previously written newlines during the second call so that we end up with the + /// correct number overall. + /// + /// - Parameter newlines: The number and type of newlines to write. + mutating func writeNewlines(_ newlines: NewlineBehavior) { + let numberToPrint: Int + switch newlines { + case .elective: + numberToPrint = consecutiveNewlineCount == 0 ? 1 : 0 + case .soft(let count, _): + // We add 1 to the max blank lines because it takes 2 newlines to create the first blank line. + numberToPrint = min(count, maximumBlankLines + 1) - consecutiveNewlineCount + case .hard(let count): + numberToPrint = count + } + + guard numberToPrint > 0 else { return } + writeRaw(String(repeating: "\n", count: numberToPrint)) + lineNumber += numberToPrint + isAtStartOfLine = true + consecutiveNewlineCount += numberToPrint + pendingSpaces = 0 + column = 0 + } + + /// Writes the given text to the output stream. + /// + /// Before printing the text, this function will print any line-leading indentation or interior + /// leading spaces that are required before the text itself. + mutating func write(_ text: String) { + if isAtStartOfLine { + writeRaw(currentIndentation.indentation()) + column = currentIndentation.length(tabWidth: tabWidth) + isAtStartOfLine = false + } else if pendingSpaces > 0 { + writeRaw(String(repeating: " ", count: pendingSpaces)) + } + writeRaw(text) + consecutiveNewlineCount = 0 + pendingSpaces = 0 + column += text.count + } + + /// Request that the given number of spaces be printed out before the next text token. + /// + /// Spaces are printed only when the next text token is printed in order to prevent us from + /// printing lines that are only whitespace or have trailing whitespace. + mutating func enqueueSpaces(_ count: Int) { + pendingSpaces += count + column += count + } + + mutating func writeVerbatim(_ verbatim: String, _ length: Int) { + writeRaw(verbatim) + consecutiveNewlineCount = 0 + pendingSpaces = 0 + column += length + } + + /// Calls writeRaw, but also updates some state variables that are normally tracked by + /// higher level functions. This is used when we switch from disabled formatting to + /// enabled formatting, writing all the previous information as-is. + mutating func writeVerbatimAfterEnablingFormatting(_ str: S) { + writeRaw(str) + if str.hasSuffix("\n") { + isAtStartOfLine = true + consecutiveNewlineCount = 1 + } else { + isAtStartOfLine = false + consecutiveNewlineCount = 0 + } + } + + /// Append the given string to the output buffer. + /// + /// No further processing is performed on the string. + private mutating func writeRaw(_ str: S) { + guard isEnabled else { return } + output.append(String(str)) + } +} diff --git a/Sources/SwiftFormat/PrettyPrint/Token.swift b/Sources/SwiftFormat/PrettyPrint/Token.swift index fdfcb297f..277317c62 100644 --- a/Sources/SwiftFormat/PrettyPrint/Token.swift +++ b/Sources/SwiftFormat/PrettyPrint/Token.swift @@ -10,6 +10,8 @@ // //===----------------------------------------------------------------------===// +import SwiftSyntax + enum GroupBreakStyle { /// A consistent break indicates that the break will always be finalized as a newline /// if wrapping occurs. @@ -196,6 +198,13 @@ enum Token { /// Ends a scope where `contextual` breaks have consistent behavior. case contextualBreakingEnd + /// Turn formatting back on at the given position in the original file + /// nil is used to indicate the rest of the file should be output + case enableFormatting(AbsolutePosition?) + + /// Turn formatting off at the given position in the original file. + case disableFormatting(AbsolutePosition) + // Convenience overloads for the enum types static let open = Token.open(.inconsistent, 0) diff --git a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift index 08f1aa4f0..9f71a4fbf 100644 --- a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift @@ -34,6 +34,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { private let config: Configuration private let operatorTable: OperatorTable private let maxlinelength: Int + private let selection: Selection /// The index of the most recently appended break, or nil when no break has been appended. private var lastBreakIndex: Int? = nil @@ -68,17 +69,32 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { /// in a function call containing multiple trailing closures). private var forcedBreakingClosures = Set() - init(configuration: Configuration, operatorTable: OperatorTable) { + /// Tracks whether we last considered ourselves inside the selection + private var isInsideSelection = true + + init(configuration: Configuration, selection: Selection, operatorTable: OperatorTable) { self.config = configuration + self.selection = selection self.operatorTable = operatorTable self.maxlinelength = config.lineLength super.init(viewMode: .all) } func makeStream(from node: Syntax) -> [Token] { + // if we have a selection, then we start outside of it + if case .ranges = selection { + appendToken(.disableFormatting(AbsolutePosition(utf8Offset: 0))) + isInsideSelection = false + } + // Because `walk` takes an `inout` argument, and we're a class, we have to do the following // dance to pass ourselves in. self.walk(node) + + // Make sure we output any trailing text after the last selection range + if case .ranges = selection { + appendToken(.enableFormatting(nil)) + } defer { tokens = [] } return tokens } @@ -837,7 +853,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { } override func visit(_ node: YieldStmtSyntax) -> SyntaxVisitorContinueKind { - // As of https://github.com/apple/swift-syntax/pull/895, the token following a `yield` keyword + // As of https://github.com/swiftlang/swift-syntax/pull/895, the token following a `yield` keyword // *must* be on the same line, so we cannot break here. after(node.yieldKeyword, tokens: .space) return .visitChildren @@ -1802,6 +1818,11 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { return .visitChildren } + override func visit(_ node: ExposeAttributeArgumentsSyntax) -> SyntaxVisitorContinueKind { + after(node.comma, tokens: .break(.same, size: 1)) + return .visitChildren + } + override func visit(_ node: AvailabilityLabeledArgumentSyntax) -> SyntaxVisitorContinueKind { before(node.label, tokens: .open) @@ -2719,11 +2740,17 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { extractLeadingTrivia(token) closeScopeTokens.forEach(appendToken) + generateEnableFormattingIfNecessary( + token.positionAfterSkippingLeadingTrivia ..< token.endPositionBeforeTrailingTrivia + ) + if !ignoredTokens.contains(token) { // Otherwise, it's just a regular token, so add the text as-is. appendToken(.syntax(token.presence == .present ? token.text : "")) } + generateDisableFormattingIfNecessary(token.endPositionBeforeTrailingTrivia) + appendTrailingTrivia(token) appendAfterTokensAndTrailingComments(token) @@ -2731,6 +2758,22 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { return .skipChildren } + private func generateEnableFormattingIfNecessary(_ range: Range) { + if case .infinite = selection { return } + if !isInsideSelection && selection.overlapsOrTouches(range) { + appendToken(.enableFormatting(range.lowerBound)) + isInsideSelection = true + } + } + + private func generateDisableFormattingIfNecessary(_ position: AbsolutePosition) { + if case .infinite = selection { return } + if isInsideSelection && !selection.contains(position) { + appendToken(.disableFormatting(position)) + isInsideSelection = false + } + } + /// Appends the before-tokens of the given syntax token to the token stream. private func appendBeforeTokens(_ token: TokenSyntax) { if let before = beforeMap.removeValue(forKey: token) { @@ -3194,11 +3237,14 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { private func extractLeadingTrivia(_ token: TokenSyntax) { var isStartOfFile: Bool let trivia: Trivia + var position = token.position if let previousToken = token.previousToken(viewMode: .sourceAccurate) { isStartOfFile = false // Find the first non-whitespace in the previous token's trailing and peel those off. let (_, prevTrailingComments) = partitionTrailingTrivia(previousToken.trailingTrivia) - trivia = Trivia(pieces: prevTrailingComments) + token.leadingTrivia + let prevTrivia = Trivia(pieces: prevTrailingComments) + trivia = prevTrivia + token.leadingTrivia + position -= prevTrivia.sourceLength } else { isStartOfFile = true trivia = token.leadingTrivia @@ -3229,7 +3275,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { switch piece { case .lineComment(let text): if index > 0 || isStartOfFile { + generateEnableFormattingIfNecessary(position ..< position + piece.sourceLength) appendToken(.comment(Comment(kind: .line, text: text), wasEndOfLine: false)) + generateDisableFormattingIfNecessary(position + piece.sourceLength) appendNewlines(.soft) isStartOfFile = false } @@ -3237,7 +3285,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { case .blockComment(let text): if index > 0 || isStartOfFile { + generateEnableFormattingIfNecessary(position ..< position + piece.sourceLength) appendToken(.comment(Comment(kind: .block, text: text), wasEndOfLine: false)) + generateDisableFormattingIfNecessary(position + piece.sourceLength) // There is always a break after the comment to allow a discretionary newline after it. var breakSize = 0 if index + 1 < trivia.endIndex { @@ -3252,13 +3302,17 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { requiresNextNewline = false case .docLineComment(let text): + generateEnableFormattingIfNecessary(position ..< position + piece.sourceLength) appendToken(.comment(Comment(kind: .docLine, text: text), wasEndOfLine: false)) + generateDisableFormattingIfNecessary(position + piece.sourceLength) appendNewlines(.soft) isStartOfFile = false requiresNextNewline = true case .docBlockComment(let text): + generateEnableFormattingIfNecessary(position ..< position + piece.sourceLength) appendToken(.comment(Comment(kind: .docBlock, text: text), wasEndOfLine: false)) + generateDisableFormattingIfNecessary(position + piece.sourceLength) appendNewlines(.soft) isStartOfFile = false requiresNextNewline = false @@ -3297,6 +3351,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { default: break } + position += piece.sourceLength } } @@ -3432,7 +3487,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { case .break: lastBreakIndex = tokens.endIndex canMergeNewlinesIntoLastBreak = true - case .open, .printerControl, .contextualBreakingStart: + case .open, .printerControl, .contextualBreakingStart, .enableFormatting, .disableFormatting: break default: canMergeNewlinesIntoLastBreak = false @@ -3997,10 +4052,17 @@ private func isNestedInPostfixIfConfig(node: Syntax) -> Bool { extension Syntax { /// Creates a pretty-printable token stream for the provided Syntax node. - func makeTokenStream(configuration: Configuration, operatorTable: OperatorTable) -> [Token] { - let commentsMoved = CommentMovingRewriter().rewrite(self) - return TokenStreamCreator(configuration: configuration, operatorTable: operatorTable) - .makeStream(from: commentsMoved) + func makeTokenStream( + configuration: Configuration, + selection: Selection, + operatorTable: OperatorTable + ) -> [Token] { + let commentsMoved = CommentMovingRewriter(selection: selection).rewrite(self) + return TokenStreamCreator( + configuration: configuration, + selection: selection, + operatorTable: operatorTable + ).makeStream(from: commentsMoved) } } @@ -4010,6 +4072,12 @@ extension Syntax { /// For example, comments after binary operators are relocated to be before the operator, which /// results in fewer line breaks with the comment closer to the relevant tokens. class CommentMovingRewriter: SyntaxRewriter { + init(selection: Selection = .infinite) { + self.selection = selection + } + + private let selection: Selection + override func visit(_ node: SourceFileSyntax) -> SourceFileSyntax { if shouldFormatterIgnore(file: node) { return node @@ -4018,14 +4086,14 @@ class CommentMovingRewriter: SyntaxRewriter { } override func visit(_ node: CodeBlockItemSyntax) -> CodeBlockItemSyntax { - if shouldFormatterIgnore(node: Syntax(node)) { + if shouldFormatterIgnore(node: Syntax(node)) || !Syntax(node).isInsideSelection(selection) { return node } return super.visit(node) } override func visit(_ node: MemberBlockItemSyntax) -> MemberBlockItemSyntax { - if shouldFormatterIgnore(node: Syntax(node)) { + if shouldFormatterIgnore(node: Syntax(node)) || !Syntax(node).isInsideSelection(selection) { return node } return super.visit(node) diff --git a/Sources/SwiftFormat/Rules/OrderedImports.swift b/Sources/SwiftFormat/Rules/OrderedImports.swift index 292bbd472..6d2475a1b 100644 --- a/Sources/SwiftFormat/Rules/OrderedImports.swift +++ b/Sources/SwiftFormat/Rules/OrderedImports.swift @@ -310,7 +310,7 @@ fileprivate func generateLines(codeBlockItemList: CodeBlockItemListSyntax, conte if currentLine.syntaxNode != nil { appendNewLine() } - let sortable = context.isRuleEnabled(OrderedImports.self, node: Syntax(block)) + let sortable = context.shouldFormat(OrderedImports.self, node: Syntax(block)) var blockWithoutTrailingTrivia = block blockWithoutTrailingTrivia.trailingTrivia = [] currentLine.syntaxNode = .importCodeBlock(blockWithoutTrailingTrivia, sortable: sortable) diff --git a/Sources/_SwiftFormatTestSupport/DiagnosingTestCase.swift b/Sources/_SwiftFormatTestSupport/DiagnosingTestCase.swift index f7a9b25a8..1c8054d23 100644 --- a/Sources/_SwiftFormatTestSupport/DiagnosingTestCase.swift +++ b/Sources/_SwiftFormatTestSupport/DiagnosingTestCase.swift @@ -15,6 +15,7 @@ open class DiagnosingTestCase: XCTestCase { public func makeContext( sourceFileSyntax: SourceFileSyntax, configuration: Configuration? = nil, + selection: Selection, findingConsumer: @escaping (Finding) -> Void ) -> Context { let context = Context( @@ -22,6 +23,7 @@ open class DiagnosingTestCase: XCTestCase { operatorTable: .standardOperators, findingConsumer: findingConsumer, fileURL: URL(fileURLWithPath: "/tmp/test.swift"), + selection: selection, sourceFileSyntax: sourceFileSyntax, ruleNameCache: ruleNameCache) return context diff --git a/Sources/_SwiftFormatTestSupport/MarkedText.swift b/Sources/_SwiftFormatTestSupport/MarkedText.swift index e43c8ccf8..071a7540a 100644 --- a/Sources/_SwiftFormatTestSupport/MarkedText.swift +++ b/Sources/_SwiftFormatTestSupport/MarkedText.swift @@ -10,6 +10,9 @@ // //===----------------------------------------------------------------------===// +import SwiftSyntax +import SwiftFormat + /// Encapsulates the locations of emoji markers extracted from source text. public struct MarkedText { /// A mapping from marker names to the UTF-8 offset where the marker was found in the string. @@ -18,23 +21,35 @@ public struct MarkedText { /// The text with all markers removed. public let textWithoutMarkers: String + /// If the marked text contains "⏩" and "⏪", they're used to create a selection + public var selection: Selection + /// Creates a new `MarkedText` value by extracting emoji markers from the given text. public init(textWithMarkers markedText: String) { var text = "" var markers = [String: Int]() var lastIndex = markedText.startIndex + var offsets = [Range]() + var lastRangeStart = 0 for marker in findMarkedRanges(in: markedText) { text += markedText[lastIndex.."), - configuration: configuration) + configuration: configuration, + selection: Selection(offsetRanges: lintFormatOptions.offsets)) processFile(fileToProcess) } @@ -166,7 +176,12 @@ class Frontend { return nil } - return FileToProcess(fileHandle: sourceFile, url: url, configuration: configuration) + return FileToProcess( + fileHandle: sourceFile, + url: url, + configuration: configuration, + selection: Selection(offsetRanges: lintFormatOptions.offsets) + ) } /// Returns the configuration that applies to the given `.swift` source file, when an explicit diff --git a/Sources/swift-format/Subcommands/LintFormatOptions.swift b/Sources/swift-format/Subcommands/LintFormatOptions.swift index 4e98d1e14..098ad25d1 100644 --- a/Sources/swift-format/Subcommands/LintFormatOptions.swift +++ b/Sources/swift-format/Subcommands/LintFormatOptions.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 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 @@ -26,6 +26,17 @@ struct LintFormatOptions: ParsableArguments { """) var configuration: String? + /// A list of comma-separated "start:end" pairs specifying UTF-8 offsets of the ranges to format. + /// + /// If not specified, the whole file will be formatted. + @Option( + name: .long, + help: """ + A "start:end" pair specifying UTF-8 offsets of the range to format. Multiple ranges can be + formatted by specifying several --offsets arguments. + """) + var offsets: [Range] = [] + /// The filename for the source code when reading from standard input, to include in diagnostic /// messages. /// @@ -94,6 +105,10 @@ struct LintFormatOptions: ParsableArguments { throw ValidationError("'--assume-filename' is only valid when reading from stdin") } + if !offsets.isEmpty && paths.count > 1 { + throw ValidationError("'--offsets' is only valid when processing a single file") + } + if !paths.isEmpty && !recursive { for path in paths { var isDir: ObjCBool = false @@ -109,3 +124,20 @@ struct LintFormatOptions: ParsableArguments { } } } + +extension Range { + public init?(argument: String) { + let pair = argument.components(separatedBy: ":") + if pair.count == 2, let start = Int(pair[0]), let end = Int(pair[1]), start <= end { + self = start ..< end + } else { + return nil + } + } +} + +#if compiler(>=6) +extension Range : @retroactive ExpressibleByArgument {} +#else +extension Range : ExpressibleByArgument {} +#endif diff --git a/Tests/SwiftFormatPerformanceTests/WhitespaceLinterPerformanceTests.swift b/Tests/SwiftFormatPerformanceTests/WhitespaceLinterPerformanceTests.swift index 938fdad49..960033604 100644 --- a/Tests/SwiftFormatPerformanceTests/WhitespaceLinterPerformanceTests.swift +++ b/Tests/SwiftFormatPerformanceTests/WhitespaceLinterPerformanceTests.swift @@ -58,7 +58,8 @@ final class WhitespaceLinterPerformanceTests: DiagnosingTestCase { /// - expected: The formatted text. private func performWhitespaceLint(input: String, expected: String) { let sourceFileSyntax = Parser.parse(source: input) - let context = makeContext(sourceFileSyntax: sourceFileSyntax, findingConsumer: { _ in }) + let context = makeContext(sourceFileSyntax: sourceFileSyntax, selection: .infinite, + findingConsumer: { _ in }) let linter = WhitespaceLinter(user: input, formatted: expected, context: context) linter.lint() } diff --git a/Tests/SwiftFormatTests/Core/DocumentationCommentTextTests.swift b/Tests/SwiftFormatTests/Core/DocumentationCommentTextTests.swift index adda6e4f1..4a1f8302f 100644 --- a/Tests/SwiftFormatTests/Core/DocumentationCommentTextTests.swift +++ b/Tests/SwiftFormatTests/Core/DocumentationCommentTextTests.swift @@ -54,7 +54,25 @@ final class DocumentationCommentTextTests: XCTestCase { """ ) } - + + func testIndentedDocBlockCommentWithASCIIArt() throws { + let decl: DeclSyntax = """ + /** + * A simple doc comment. + */ + func f() {} + """ + let commentText = try XCTUnwrap(DocumentationCommentText(extractedFrom: decl.leadingTrivia)) + XCTAssertEqual(commentText.introducer, .block) + XCTAssertEqual( + commentText.text, + """ + A simple doc comment. + + """ + ) + } + func testDocBlockCommentWithoutASCIIArt() throws { let decl: DeclSyntax = """ /** diff --git a/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift b/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift index 3031fc31b..3ff5db02d 100644 --- a/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift +++ b/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift @@ -444,4 +444,28 @@ final class AttributeTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 100) } + + func testAttributeParamSpacingInExpose() { + let input = + """ + @_expose( wasm , "foo" ) + func f() {} + + @_expose( Cxx , "bar") + func b() {} + + """ + + let expected = + """ + @_expose(wasm, "foo") + func f() {} + + @_expose(Cxx, "bar") + func b() {} + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 100) + } } diff --git a/Tests/SwiftFormatTests/PrettyPrint/IgnoreNodeTests.swift b/Tests/SwiftFormatTests/PrettyPrint/IgnoreNodeTests.swift index 4e51f4de5..3153ccd80 100644 --- a/Tests/SwiftFormatTests/PrettyPrint/IgnoreNodeTests.swift +++ b/Tests/SwiftFormatTests/PrettyPrint/IgnoreNodeTests.swift @@ -1,5 +1,5 @@ final class IgnoreNodeTests: PrettyPrintTestCase { - func atestIgnoreCodeBlockListItems() { + func testIgnoreCodeBlockListItems() { let input = """ x = 4 + 5 // This comment stays here. diff --git a/Tests/SwiftFormatTests/PrettyPrint/PrettyPrintTestCase.swift b/Tests/SwiftFormatTests/PrettyPrint/PrettyPrintTestCase.swift index 3aa54af95..eaa33ac3a 100644 --- a/Tests/SwiftFormatTests/PrettyPrint/PrettyPrintTestCase.swift +++ b/Tests/SwiftFormatTests/PrettyPrint/PrettyPrintTestCase.swift @@ -43,6 +43,7 @@ class PrettyPrintTestCase: DiagnosingTestCase { let (formatted, context) = prettyPrintedSource( markedInput.textWithoutMarkers, configuration: configuration, + selection: markedInput.selection, whitespaceOnly: whitespaceOnly, findingConsumer: { emittedFindings.append($0) }) assertStringsEqualWithDiff( @@ -64,14 +65,18 @@ class PrettyPrintTestCase: DiagnosingTestCase { // Idempotency check: Running the formatter multiple times should not change the outcome. // Assert that running the formatter again on the previous result keeps it the same. - let (reformatted, _) = prettyPrintedSource( - formatted, - configuration: configuration, - whitespaceOnly: whitespaceOnly, - findingConsumer: { _ in } // Ignore findings during the idempotence check. - ) - assertStringsEqualWithDiff( - reformatted, formatted, "Pretty printer is not idempotent", file: file, line: line) + // But if we have ranges, they aren't going to be valid for the formatted text. + if case .infinite = markedInput.selection { + let (reformatted, _) = prettyPrintedSource( + formatted, + configuration: configuration, + selection: markedInput.selection, + whitespaceOnly: whitespaceOnly, + findingConsumer: { _ in } // Ignore findings during the idempotence check. + ) + assertStringsEqualWithDiff( + reformatted, formatted, "Pretty printer is not idempotent", file: file, line: line) + } } /// Returns the given source code reformatted with the pretty printer. @@ -86,6 +91,7 @@ class PrettyPrintTestCase: DiagnosingTestCase { private func prettyPrintedSource( _ source: String, configuration: Configuration, + selection: Selection, whitespaceOnly: Bool, findingConsumer: @escaping (Finding) -> Void ) -> (String, Context) { @@ -96,9 +102,11 @@ class PrettyPrintTestCase: DiagnosingTestCase { let context = makeContext( sourceFileSyntax: sourceFileSyntax, configuration: configuration, + selection: selection, findingConsumer: findingConsumer) let printer = PrettyPrinter( context: context, + source: source, node: Syntax(sourceFileSyntax), printTokenStream: false, whitespaceOnly: whitespaceOnly) diff --git a/Tests/SwiftFormatTests/PrettyPrint/SelectionTests.swift b/Tests/SwiftFormatTests/PrettyPrint/SelectionTests.swift new file mode 100644 index 000000000..fb95b2a6e --- /dev/null +++ b/Tests/SwiftFormatTests/PrettyPrint/SelectionTests.swift @@ -0,0 +1,395 @@ +import SwiftFormat +import XCTest + +final class SelectionTests: PrettyPrintTestCase { + func testSelectAll() { + let input = + """ + ⏩func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + } + }⏪ + """ + + let expected = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + } + } + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + func testSelectComment() { + let input = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + ⏩// do stuff⏪ + } + } + """ + + let expected = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + } + } + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + func testInsertionPointBeforeComment() { + let input = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + ⏩⏪// do stuff + } + } + """ + + let expected = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + } + } + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + func testSpacesInline() { + let input = + """ + func foo() { + if let SomeReallyLongVar ⏩ = ⏪Some.More.Stuff(), let a = myfunc() { + // do stuff + } + } + """ + + let expected = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + } + } + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + func testSpacesFullLine() { + let input = + """ + func foo() { + ⏩if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() {⏪ + // do stuff + } + } + """ + + let expected = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + } + } + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + func testWrapInline() { + let input = + """ + func foo() { + if let SomeReallyLongVar = ⏩Some.More.Stuff(), let a = myfunc()⏪ { + // do stuff + } + } + """ + + let expected = + """ + func foo() { + if let SomeReallyLongVar = Some.More + .Stuff(), let a = myfunc() { + // do stuff + } + } + """ + + // The line length ends on the last paren of .Stuff() + assertPrettyPrintEqual(input: input, expected: expected, linelength: 44) + } + + func testCommentsOnly() { + let input = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + ⏩// do stuff + // do more stuff⏪ + var i = 0 + } + } + """ + + let expected = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + // do more stuff + var i = 0 + } + } + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + func testVarOnly() { + let input = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + // do more stuff + ⏩⏪var i = 0 + } + } + """ + + let expected = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + // do more stuff + var i = 0 + } + } + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + func testSingleLineFunc() { + let input = + """ + func foo() ⏩{}⏪ + """ + + let expected = + """ + func foo() {} + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + func testSingleLineFunc2() { + let input = + """ + func foo() /**/ ⏩{}⏪ + """ + + let expected = + """ + func foo() /**/ {} + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + func testSimpleFunc() { + let input = + """ + func foo() /**/ + ⏩{}⏪ + """ + + let expected = + """ + func foo() /**/ + {} + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + // MARK: - multiple selection ranges + func testFirstCommentAndVar() { + let input = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + ⏩⏪// do stuff + // do more stuff + ⏩⏪var i = 0 + } + } + """ + + let expected = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + // do more stuff + var i = 0 + } + } + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + // from AccessorTests (but with some Selection ranges) + func testBasicAccessors() { + let input = + """ + ⏩struct MyStruct { + var memberValue: Int + var someValue: Int { get { return memberValue + 2 } set(newValue) { memberValue = newValue } } + }⏪ + struct MyStruct { + var memberValue: Int + var someValue: Int { @objc get { return memberValue + 2 } @objc(isEnabled) set(newValue) { memberValue = newValue } } + } + struct MyStruct { + var memberValue: Int + var memberValue2: Int + var someValue: Int { + get { + let A = 123 + return A + } + set(newValue) { + memberValue = newValue && otherValue + ⏩memberValue2 = newValue / 2 && andableValue⏪ + } + } + } + struct MyStruct { + var memberValue: Int + var SomeValue: Int { return 123 } + var AnotherValue: Double { + let out = 1.23 + return out + } + } + """ + + let expected = + """ + struct MyStruct { + var memberValue: Int + var someValue: Int { + get { return memberValue + 2 } + set(newValue) { memberValue = newValue } + } + } + struct MyStruct { + var memberValue: Int + var someValue: Int { @objc get { return memberValue + 2 } @objc(isEnabled) set(newValue) { memberValue = newValue } } + } + struct MyStruct { + var memberValue: Int + var memberValue2: Int + var someValue: Int { + get { + let A = 123 + return A + } + set(newValue) { + memberValue = newValue && otherValue + memberValue2 = + newValue / 2 && andableValue + } + } + } + struct MyStruct { + var memberValue: Int + var SomeValue: Int { return 123 } + var AnotherValue: Double { + let out = 1.23 + return out + } + } + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 45) + } + + // from CommentTests (but with some Selection ranges) + func testContainerLineComments() { + let input = + """ + // Array comment + let a = [⏩4⏪56, // small comment + 789] + + // Dictionary comment + let b = ["abc": ⏩456, // small comment + "def": 789]⏪ + + // Trailing comment + let c = [123, 456 // small comment + ] + + ⏩/* Array comment */ + let a = [456, /* small comment */ + 789] + + /* Dictionary comment */ + let b = ["abc": 456, /* small comment */ + "def": 789]⏪ + """ + + let expected = + """ + // Array comment + let a = [ + 456, // small comment + 789] + + // Dictionary comment + let b = ["abc": 456, // small comment + "def": 789, + ] + + // Trailing comment + let c = [123, 456 // small comment + ] + + /* Array comment */ + let a = [ + 456, /* small comment */ + 789, + ] + + /* Dictionary comment */ + let b = [ + "abc": 456, /* small comment */ + "def": 789, + ] + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } +} diff --git a/Tests/SwiftFormatTests/PrettyPrint/WhitespaceTestCase.swift b/Tests/SwiftFormatTests/PrettyPrint/WhitespaceTestCase.swift index 1add23434..78c49752e 100644 --- a/Tests/SwiftFormatTests/PrettyPrint/WhitespaceTestCase.swift +++ b/Tests/SwiftFormatTests/PrettyPrint/WhitespaceTestCase.swift @@ -39,6 +39,7 @@ class WhitespaceTestCase: DiagnosingTestCase { let context = makeContext( sourceFileSyntax: sourceFileSyntax, configuration: configuration, + selection: .infinite, findingConsumer: { emittedFindings.append($0) }) let linter = WhitespaceLinter( user: markedText.textWithoutMarkers, formatted: expected, context: context) diff --git a/Tests/SwiftFormatTests/Rules/BeginDocumentationCommentWithOneLineSummaryTests.swift b/Tests/SwiftFormatTests/Rules/BeginDocumentationCommentWithOneLineSummaryTests.swift index e41425930..cfeaa09dd 100644 --- a/Tests/SwiftFormatTests/Rules/BeginDocumentationCommentWithOneLineSummaryTests.swift +++ b/Tests/SwiftFormatTests/Rules/BeginDocumentationCommentWithOneLineSummaryTests.swift @@ -14,33 +14,33 @@ final class BeginDocumentationCommentWithOneLineSummaryTests: LintOrFormatRuleTe assertLint( BeginDocumentationCommentWithOneLineSummary.self, """ - /// Returns a bottle of Dr Pepper from the vending machine. - public func drPepper(from vendingMachine: VendingMachine) -> Soda {} + /// Returns a bottle of Dr Pepper from the vending machine. + public func drPepper(from vendingMachine: VendingMachine) -> Soda {} - /// Contains a comment as description that needs a sentence - /// of two lines of code. - public var twoLinesForOneSentence = "test" + /// Contains a comment as description that needs a sentence + /// of two lines of code. + public var twoLinesForOneSentence = "test" - /// The background color of the view. - var backgroundColor: UIColor + /// The background color of the view. + var backgroundColor: UIColor - /// Returns the sum of the numbers. - /// - /// - Parameter numbers: The numbers to sum. - /// - Returns: The sum of the numbers. - func sum(_ numbers: [Int]) -> Int { - // ... - } + /// Returns the sum of the numbers. + /// + /// - Parameter numbers: The numbers to sum. + /// - Returns: The sum of the numbers. + func sum(_ numbers: [Int]) -> Int { + // ... + } - /// This docline should not succeed. - /// There are two sentences without a blank line between them. - 1️⃣struct Test {} + /// This docline should not succeed. + /// There are two sentences without a blank line between them. + 1️⃣struct Test {} - /// This docline should not succeed. There are two sentences. - 2️⃣public enum Token { case comma, semicolon, identifier } + /// This docline should not succeed. There are two sentences. + 2️⃣public enum Token { case comma, semicolon, identifier } - /// Should fail because it doesn't have a period - 3️⃣public class testNoPeriod {} + /// Should fail because it doesn't have a period + 3️⃣public class testNoPeriod {} """, findings: [ FindingSpec("1️⃣", message: #"add a blank comment line after this sentence: "This docline should not succeed.""#), @@ -54,36 +54,36 @@ final class BeginDocumentationCommentWithOneLineSummaryTests: LintOrFormatRuleTe assertLint( BeginDocumentationCommentWithOneLineSummary.self, """ - /** - * Returns the numeric value. - * - * - Parameters: - * - digit: The Unicode scalar whose numeric value should be returned. - * - radix: The radix, between 2 and 36, used to compute the numeric value. - * - Returns: The numeric value of the scalar.*/ - func numericValue(of digit: UnicodeScalar, radix: Int = 10) -> Int {} - - /** - * This block comment contains a sentence summary - * of two lines of code. - */ - public var twoLinesForOneSentence = "test" - - /** - * This block comment should not succeed, struct. - * There are two sentences without a blank line between them. - */ - 1️⃣struct TestStruct {} - - /** - This block comment should not succeed, class. - Add a blank comment after the first line. - */ - 2️⃣public class TestClass {} - /** This block comment should not succeed, enum. There are two sentences. */ - 3️⃣public enum testEnum {} - /** Should fail because it doesn't have a period */ - 4️⃣public class testNoPeriod {} + /** + * Returns the numeric value. + * + * - Parameters: + * - digit: The Unicode scalar whose numeric value should be returned. + * - radix: The radix, between 2 and 36, used to compute the numeric value. + * - Returns: The numeric value of the scalar.*/ + func numericValue(of digit: UnicodeScalar, radix: Int = 10) -> Int {} + + /** + * This block comment contains a sentence summary + * of two lines of code. + */ + public var twoLinesForOneSentence = "test" + + /** + * This block comment should not succeed, struct. + * There are two sentences without a blank line between them. + */ + 1️⃣struct TestStruct {} + + /** + This block comment should not succeed, class. + Add a blank comment after the first line. + */ + 2️⃣public class TestClass {} + /** This block comment should not succeed, enum. There are two sentences. */ + 3️⃣public enum testEnum {} + /** Should fail because it doesn't have a period */ + 4️⃣public class testNoPeriod {} """, findings: [ FindingSpec("1️⃣", message: #"add a blank comment line after this sentence: "This block comment should not succeed, struct.""#), diff --git a/Tests/SwiftFormatTests/Rules/LintOrFormatRuleTestCase.swift b/Tests/SwiftFormatTests/Rules/LintOrFormatRuleTestCase.swift index e989bd804..814952e6d 100644 --- a/Tests/SwiftFormatTests/Rules/LintOrFormatRuleTestCase.swift +++ b/Tests/SwiftFormatTests/Rules/LintOrFormatRuleTestCase.swift @@ -27,7 +27,8 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase { line: UInt = #line ) { let markedText = MarkedText(textWithMarkers: markedSource) - let tree = Parser.parse(source: markedText.textWithoutMarkers) + let unmarkedSource = markedText.textWithoutMarkers + let tree = Parser.parse(source: unmarkedSource) let sourceFileSyntax = try! OperatorTable.standardOperators.foldAll(tree).as(SourceFileSyntax.self)! @@ -39,6 +40,7 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase { let context = makeContext( sourceFileSyntax: sourceFileSyntax, configuration: configuration, + selection: .infinite, findingConsumer: { emittedFindings.append($0) }) let linter = type.init(context: context) linter.walk(sourceFileSyntax) @@ -60,6 +62,7 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase { pipeline.debugOptions.insert(.disablePrettyPrint) try! pipeline.lint( syntax: sourceFileSyntax, + source: unmarkedSource, operatorTable: OperatorTable.standardOperators, assumingFileURL: URL(string: file.description)!) @@ -96,7 +99,8 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase { line: UInt = #line ) { let markedInput = MarkedText(textWithMarkers: input) - let tree = Parser.parse(source: markedInput.textWithoutMarkers) + let originalSource: String = markedInput.textWithoutMarkers + let tree = Parser.parse(source: originalSource) let sourceFileSyntax = try! OperatorTable.standardOperators.foldAll(tree).as(SourceFileSyntax.self)! @@ -108,6 +112,7 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase { let context = makeContext( sourceFileSyntax: sourceFileSyntax, configuration: configuration, + selection: .infinite, findingConsumer: { emittedFindings.append($0) }) let formatter = formatType.init(context: context) @@ -129,6 +134,7 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase { // misplacing trivia in a way that the pretty-printer isn't able to handle). let prettyPrintedSource = PrettyPrinter( context: context, + source: originalSource, node: Syntax(actual), printTokenStream: false, whitespaceOnly: false @@ -148,8 +154,8 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase { pipeline.debugOptions.insert(.disablePrettyPrint) var pipelineActual = "" try! pipeline.format( - syntax: sourceFileSyntax, operatorTable: OperatorTable.standardOperators, - assumingFileURL: nil, to: &pipelineActual) + syntax: sourceFileSyntax, source: originalSource, operatorTable: OperatorTable.standardOperators, + assumingFileURL: nil, selection: .infinite, to: &pipelineActual) assertStringsEqualWithDiff(pipelineActual, expected) assertFindings( expected: findings, markerLocations: markedInput.markers, diff --git a/cmake/modules/SwiftSupport.cmake b/cmake/modules/SwiftSupport.cmake index 35decfcca..c37055a33 100644 --- a/cmake/modules/SwiftSupport.cmake +++ b/cmake/modules/SwiftSupport.cmake @@ -47,6 +47,8 @@ function(get_swift_host_arch result_var_name) set("${result_var_name}" "i686" PARENT_SCOPE) elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "wasm32") set("${result_var_name}" "wasm32" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "riscv64") + set("${result_var_name}" "riscv64" PARENT_SCOPE) else() message(FATAL_ERROR "Unrecognized architecture on host system: ${CMAKE_SYSTEM_PROCESSOR}") endif()