From eec64033cfcebb51c2b4c322ebb9233bade545fa Mon Sep 17 00:00:00 2001 From: Shawn Hyam Date: Tue, 28 May 2024 11:03:01 -0400 Subject: [PATCH 01/13] Handle indented block comments with ASCII art correctly. --- .../Core/DocumentationCommentText.swift | 53 +++++---- .../Core/DocumentationCommentTextTests.swift | 20 +++- ...tationCommentWithOneLineSummaryTests.swift | 102 +++++++++--------- 3 files changed, 105 insertions(+), 70 deletions(-) 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/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/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.""#), From 66400670c77c7ac91adb9e526b267e7003288a6d Mon Sep 17 00:00:00 2001 From: David Ewing Date: Thu, 21 Mar 2024 14:52:40 -0600 Subject: [PATCH 02/13] Support for formatting a selection (given as an array of ranges) . MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The basic idea here is to insert `enableFormatting` and `disableFormatting` tokens into the print stream when we enter or leave the selection. When formatting is enabled, we print out the tokens as usual. When formatting is disabled, we turn off any output until the next `enableFormatting` token. When that token is hit, we write the original source text from the location of the last `disableFormatting` to the current location. Note that this means that all the APIs need the original source text to be passed in. A `Selection` is represented as an enum with an `.infinite` case, and a `.ranges` case to indicate either selecting the entire file, or an array of start/end utf-8 offsets. The offset pairs are given with `Range`, matching the (now common) usage in swift-syntax. For testing, allow marked text to use `⏩` and `⏪` to deliniate the start/end of a range of a selection. The command line now takes an `--offsets` option of comma-separated "start:end" pairs to set the selection for formatting. --- Sources/SwiftFormat/API/Selection.swift | 63 +++ Sources/SwiftFormat/API/SwiftFormatter.swift | 31 +- Sources/SwiftFormat/API/SwiftLinter.swift | 6 +- Sources/SwiftFormat/Core/Context.swift | 11 +- .../SwiftFormat/PrettyPrint/PrettyPrint.swift | 85 ++++- Sources/SwiftFormat/PrettyPrint/Token.swift | 9 + .../PrettyPrint/TokenStreamCreator.swift | 81 +++- .../DiagnosingTestCase.swift | 2 + .../_SwiftFormatTestSupport/MarkedText.swift | 22 +- .../Frontend/FormatFrontend.swift | 4 +- Sources/swift-format/Frontend/Frontend.swift | 31 +- .../Subcommands/LintFormatOptions.swift | 32 +- .../WhitespaceLinterPerformanceTests.swift | 3 +- .../PrettyPrint/IgnoreNodeTests.swift | 2 +- .../PrettyPrint/PrettyPrintTestCase.swift | 24 +- .../PrettyPrint/SelectionTests.swift | 359 ++++++++++++++++++ .../PrettyPrint/WhitespaceTestCase.swift | 1 + .../Rules/LintOrFormatRuleTestCase.swift | 14 +- 18 files changed, 722 insertions(+), 58 deletions(-) create mode 100644 Sources/SwiftFormat/API/Selection.swift create mode 100644 Tests/SwiftFormatTests/PrettyPrint/SelectionTests.swift diff --git a/Sources/SwiftFormat/API/Selection.swift b/Sources/SwiftFormat/API/Selection.swift new file mode 100644 index 000000000..b3d51aad0 --- /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(offsetPairs: [Range]) { + if offsetPairs.isEmpty { + self = .infinite + } else { + let ranges = offsetPairs.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 { + /// return 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..9c8f6f416 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 @@ -21,6 +21,9 @@ public final class SwiftFormatter { /// The configuration settings that control the formatter's behavior. public let configuration: Configuration + /// the ranges of text to format + public var selection: Selection = .infinite + /// An optional callback that will be notified with any findings encountered during formatting. public let findingConsumer: ((Finding) -> Void)? @@ -70,6 +73,7 @@ public final class SwiftFormatter { try format( source: String(contentsOf: url, encoding: .utf8), assumingFileURL: url, + selection: .infinite, to: &outputStream, parsingDiagnosticHandler: parsingDiagnosticHandler) } @@ -86,6 +90,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 +99,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 +114,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 +128,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 +158,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/Core/Context.swift b/Sources/SwiftFormat/Core/Context.swift index 29e69b0dc..8fde15b3e 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 optional ranges 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. + /// location or not. Also makes sure the entire node is contained inside any selection. func isRuleEnabled(_ 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/PrettyPrint/PrettyPrint.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift index 1201f84c2..57c7a2a63 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,6 +67,19 @@ public class PrettyPrinter { private var configuration: Configuration { return context.configuration } private let maxLineLength: Int private var tokens: [Token] + private var source: String + + /// 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 + /// true if we're currently formatting + private var writingIsEnabled: Bool { disabledPosition == nil } + private var outputBuffer: String = "" /// The number of spaces remaining on the current line. @@ -172,11 +186,14 @@ 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 @@ -216,7 +233,9 @@ public class PrettyPrinter { } guard numberToPrint > 0 else { return } - writeRaw(String(repeating: "\n", count: numberToPrint)) + if writingIsEnabled { + writeRaw(String(repeating: "\n", count: numberToPrint)) + } lineNumber += numberToPrint isAtStartOfLine = true consecutiveNewlineCount += numberToPrint @@ -238,13 +257,17 @@ public class PrettyPrinter { /// leading spaces that are required before the text itself. private func write(_ text: String) { if isAtStartOfLine { - writeRaw(currentIndentation.indentation()) + if writingIsEnabled { + writeRaw(currentIndentation.indentation()) + } spaceRemaining = maxLineLength - currentIndentation.length(in: configuration) isAtStartOfLine = false - } else if pendingSpaces > 0 { + } else if pendingSpaces > 0 && writingIsEnabled { writeRaw(String(repeating: " ", count: pendingSpaces)) } - writeRaw(text) + if writingIsEnabled { + writeRaw(text) + } consecutiveNewlineCount = 0 pendingSpaces = 0 } @@ -523,7 +546,9 @@ public class PrettyPrinter { } case .verbatim(let verbatim): - writeRaw(verbatim.print(indent: currentIndentation)) + if writingIsEnabled { + writeRaw(verbatim.print(indent: currentIndentation)) + } consecutiveNewlineCount = 0 pendingSpaces = 0 lastBreak = false @@ -569,6 +594,40 @@ public class PrettyPrinter { write(",") spaceRemaining -= 1 } + + case .enableFormatting(let enabledPosition): + // if we're not disabled, we ignore the token + if let disabledPosition { + 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..() - 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 } @@ -2719,11 +2735,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 +2753,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 +3232,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 +3270,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 +3280,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 +3297,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 +3346,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { default: break } + position += piece.sourceLength } } @@ -3432,7 +3482,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 +4047,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 +4067,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 + } + + var selection: Selection + override func visit(_ node: SourceFileSyntax) -> SourceFileSyntax { if shouldFormatterIgnore(file: node) { return node @@ -4018,14 +4081,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/_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..8ad07572e 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) processFile(fileToProcess) } @@ -162,7 +176,16 @@ class Frontend { return nil } - return FileToProcess(fileHandle: sourceFile, url: url, configuration: configuration) + var selection: Selection = .infinite + if let offsets = lintFormatOptions.offsets { + selection = Selection(offsetPairs: offsets) + } + return FileToProcess( + fileHandle: sourceFile, + url: url, + configuration: configuration, + selection: selection + ) } /// 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..f0eca2010 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 list of comma-separated "start:end" pairs specifying UTF-8 offsets of the ranges to format. + """) + 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 == false && 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,18 @@ struct LintFormatOptions: ParsableArguments { } } } + +extension [Range] : @retroactive ExpressibleByArgument { + public init?(argument: String) { + let pairs = argument.components(separatedBy: ",") + let ranges: [Range] = pairs.compactMap { + let pair = $0.components(separatedBy: ":") + if pair.count == 2, let start = Int(pair[0]), let end = Int(pair[1]), start <= end { + return start ..< end + } else { + return nil + } + } + self = ranges + } +} 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/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..bd08d5ba3 --- /dev/null +++ b/Tests/SwiftFormatTests/PrettyPrint/SelectionTests.swift @@ -0,0 +1,359 @@ +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 + } + } + """ + + // The line length ends on the last paren of .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 + } + } + """ + + // The line length ends on the last paren of .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 + } + } + """ + + // The line length ends on the last paren of .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 + } + } + """ + + // The line length ends on the last paren of .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 + } + } + """ + + // The line length ends on the last paren of .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 + } + } + """ + + // The line length ends on the last paren of .Stuff() + 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 + } + } + """ + + // The line length ends on the last paren of .Stuff() + 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 + } + } + """ + + // The line length ends on the last paren of .Stuff() + 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/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, From 88beb8553c4b7500a3c99ea3738a0ea55528d5ae Mon Sep 17 00:00:00 2001 From: David Ewing Date: Mon, 3 Jun 2024 16:08:56 -0400 Subject: [PATCH 03/13] Some small refectorings and updates from review feedback. Add a few more test cases. For formatting a selection (). --- Sources/SwiftFormat/API/Selection.swift | 8 +- Sources/SwiftFormat/API/SwiftFormatter.swift | 3 - Sources/SwiftFormat/CMakeLists.txt | 1 + Sources/SwiftFormat/Core/Context.swift | 4 +- Sources/SwiftFormat/Core/LintPipeline.swift | 4 +- .../SwiftFormat/Core/SyntaxFormatRule.swift | 2 +- .../SwiftFormat/PrettyPrint/PrettyPrint.swift | 81 +++++++++---------- .../PrettyPrint/TokenStreamCreator.swift | 2 +- .../SwiftFormat/Rules/OrderedImports.swift | 2 +- .../_SwiftFormatTestSupport/MarkedText.swift | 2 +- Sources/swift-format/Frontend/Frontend.swift | 4 +- .../Subcommands/LintFormatOptions.swift | 8 +- .../PrettyPrint/SelectionTests.swift | 52 ++++++++++-- 13 files changed, 102 insertions(+), 71 deletions(-) diff --git a/Sources/SwiftFormat/API/Selection.swift b/Sources/SwiftFormat/API/Selection.swift index b3d51aad0..9ea599db3 100644 --- a/Sources/SwiftFormat/API/Selection.swift +++ b/Sources/SwiftFormat/API/Selection.swift @@ -19,11 +19,11 @@ public enum Selection { case ranges([Range]) /// Create a selection from an array of utf8 ranges. An empty array means an infinite selection. - public init(offsetPairs: [Range]) { - if offsetPairs.isEmpty { + public init(offsetRanges: [Range]) { + if offsetRanges.isEmpty { self = .infinite } else { - let ranges = offsetPairs.map { + let ranges = offsetRanges.map { AbsolutePosition(utf8Offset: $0.lowerBound) ..< AbsolutePosition(utf8Offset: $0.upperBound) } self = .ranges(ranges) @@ -51,7 +51,7 @@ public enum Selection { public extension Syntax { - /// return true if the node is _completely_ inside any range in the selection + /// - Returns: `true` if the node is _completely_ inside any range in the selection func isInsideSelection(_ selection: Selection) -> Bool { switch selection { case .infinite: diff --git a/Sources/SwiftFormat/API/SwiftFormatter.swift b/Sources/SwiftFormat/API/SwiftFormatter.swift index 9c8f6f416..e91030b3c 100644 --- a/Sources/SwiftFormat/API/SwiftFormatter.swift +++ b/Sources/SwiftFormat/API/SwiftFormatter.swift @@ -21,9 +21,6 @@ public final class SwiftFormatter { /// The configuration settings that control the formatter's behavior. public let configuration: Configuration - /// the ranges of text to format - public var selection: Selection = .infinite - /// An optional callback that will be notified with any findings encountered during formatting. public let findingConsumer: ((Finding) -> Void)? 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 8fde15b3e..2bbb900ac 100644 --- a/Sources/SwiftFormat/Core/Context.swift +++ b/Sources/SwiftFormat/Core/Context.swift @@ -39,7 +39,7 @@ public final class Context { /// The configuration for this run of the pipeline, provided by a configuration JSON file. let configuration: Configuration - /// The optional ranges to process + /// The selection to process let selection: Selection /// Defines the operators and their precedence relationships that were used during parsing. @@ -92,7 +92,7 @@ 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. Also makes sure the entire node is contained inside any selection. - func isRuleEnabled(_ rule: R.Type, node: Syntax) -> Bool { + func shouldFormat(_ rule: R.Type, node: Syntax) -> Bool { guard node.isInsideSelection(selection) else { return false } let loc = node.startLocation(converter: self.sourceLocationConverter) 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/PrettyPrint.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift index 57c7a2a63..0b4ff792a 100644 --- a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift @@ -69,7 +69,7 @@ public class PrettyPrinter { private var tokens: [Token] private var source: String - /// keep track of where formatting was disabled in the original source + /// 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 @@ -77,8 +77,6 @@ public class PrettyPrinter { /// current position to `outputBuffer`. From then on, we continue to format until the next /// `disableFormatting` token. private var disabledPosition: AbsolutePosition? = nil - /// true if we're currently formatting - private var writingIsEnabled: Bool { disabledPosition == nil } private var outputBuffer: String = "" @@ -204,7 +202,9 @@ public class PrettyPrinter { /// /// No further processing is performed on the string. private func writeRaw(_ str: S) { - outputBuffer.append(String(str)) + if disabledPosition == nil { + outputBuffer.append(String(str)) + } } /// Writes newlines into the output stream, taking into account any preexisting consecutive @@ -233,9 +233,7 @@ public class PrettyPrinter { } guard numberToPrint > 0 else { return } - if writingIsEnabled { - writeRaw(String(repeating: "\n", count: numberToPrint)) - } + writeRaw(String(repeating: "\n", count: numberToPrint)) lineNumber += numberToPrint isAtStartOfLine = true consecutiveNewlineCount += numberToPrint @@ -257,17 +255,13 @@ public class PrettyPrinter { /// leading spaces that are required before the text itself. private func write(_ text: String) { if isAtStartOfLine { - if writingIsEnabled { - writeRaw(currentIndentation.indentation()) - } + writeRaw(currentIndentation.indentation()) spaceRemaining = maxLineLength - currentIndentation.length(in: configuration) isAtStartOfLine = false - } else if pendingSpaces > 0 && writingIsEnabled { + } else if pendingSpaces > 0 { writeRaw(String(repeating: " ", count: pendingSpaces)) } - if writingIsEnabled { - writeRaw(text) - } + writeRaw(text) consecutiveNewlineCount = 0 pendingSpaces = 0 } @@ -546,9 +540,7 @@ public class PrettyPrinter { } case .verbatim(let verbatim): - if writingIsEnabled { - writeRaw(verbatim.print(indent: currentIndentation)) - } + writeRaw(verbatim.print(indent: currentIndentation)) consecutiveNewlineCount = 0 pendingSpaces = 0 lastBreak = false @@ -596,38 +588,37 @@ public class PrettyPrinter { } case .enableFormatting(let enabledPosition): - // if we're not disabled, we ignore the token - if let disabledPosition { - 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.. SourceFileSyntax { if shouldFormatterIgnore(file: 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/MarkedText.swift b/Sources/_SwiftFormatTestSupport/MarkedText.swift index 8ad07572e..071a7540a 100644 --- a/Sources/_SwiftFormatTestSupport/MarkedText.swift +++ b/Sources/_SwiftFormatTestSupport/MarkedText.swift @@ -49,7 +49,7 @@ public struct MarkedText { self.markers = markers self.textWithoutMarkers = text - self.selection = Selection(offsetPairs: offsets) + self.selection = Selection(offsetRanges: offsets) } } diff --git a/Sources/swift-format/Frontend/Frontend.swift b/Sources/swift-format/Frontend/Frontend.swift index f010ff582..d9cbd4e94 100644 --- a/Sources/swift-format/Frontend/Frontend.swift +++ b/Sources/swift-format/Frontend/Frontend.swift @@ -127,7 +127,7 @@ class Frontend { var selection: Selection = .infinite if let offsets = lintFormatOptions.offsets { - selection = Selection(offsetPairs: offsets) + selection = Selection(offsetRanges: offsets) } let fileToProcess = FileToProcess( fileHandle: FileHandle.standardInput, @@ -178,7 +178,7 @@ class Frontend { var selection: Selection = .infinite if let offsets = lintFormatOptions.offsets { - selection = Selection(offsetPairs: offsets) + selection = Selection(offsetRanges: offsets) } return FileToProcess( fileHandle: sourceFile, diff --git a/Sources/swift-format/Subcommands/LintFormatOptions.swift b/Sources/swift-format/Subcommands/LintFormatOptions.swift index f0eca2010..85bcc7f38 100644 --- a/Sources/swift-format/Subcommands/LintFormatOptions.swift +++ b/Sources/swift-format/Subcommands/LintFormatOptions.swift @@ -125,7 +125,7 @@ struct LintFormatOptions: ParsableArguments { } } -extension [Range] : @retroactive ExpressibleByArgument { +extension [Range] { public init?(argument: String) { let pairs = argument.components(separatedBy: ",") let ranges: [Range] = pairs.compactMap { @@ -139,3 +139,9 @@ extension [Range] : @retroactive ExpressibleByArgument { self = ranges } } + +#if compiler(>=6) +extension [Range] : @retroactive ExpressibleByArgument {} +#else +extension [Range] : ExpressibleByArgument {} +#endif diff --git a/Tests/SwiftFormatTests/PrettyPrint/SelectionTests.swift b/Tests/SwiftFormatTests/PrettyPrint/SelectionTests.swift index bd08d5ba3..fb95b2a6e 100644 --- a/Tests/SwiftFormatTests/PrettyPrint/SelectionTests.swift +++ b/Tests/SwiftFormatTests/PrettyPrint/SelectionTests.swift @@ -21,7 +21,6 @@ final class SelectionTests: PrettyPrintTestCase { } """ - // The line length ends on the last paren of .Stuff() assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) } @@ -44,7 +43,6 @@ final class SelectionTests: PrettyPrintTestCase { } """ - // The line length ends on the last paren of .Stuff() assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) } @@ -67,7 +65,6 @@ final class SelectionTests: PrettyPrintTestCase { } """ - // The line length ends on the last paren of .Stuff() assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) } @@ -90,7 +87,6 @@ final class SelectionTests: PrettyPrintTestCase { } """ - // The line length ends on the last paren of .Stuff() assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) } @@ -113,7 +109,6 @@ final class SelectionTests: PrettyPrintTestCase { } """ - // The line length ends on the last paren of .Stuff() assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) } @@ -164,7 +159,6 @@ final class SelectionTests: PrettyPrintTestCase { } """ - // The line length ends on the last paren of .Stuff() assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) } @@ -191,7 +185,50 @@ final class SelectionTests: PrettyPrintTestCase { } """ - // The line length ends on the last paren of .Stuff() + 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) } @@ -219,7 +256,6 @@ final class SelectionTests: PrettyPrintTestCase { } """ - // The line length ends on the last paren of .Stuff() assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) } From 0c0977dc4645439ae02954047e5af5b51eae78f6 Mon Sep 17 00:00:00 2001 From: David Ewing Date: Tue, 4 Jun 2024 10:22:50 -0400 Subject: [PATCH 04/13] Change the `--offsets` argument to take a single pair of offsets, and support passing multiple of them. --- Sources/swift-format/Frontend/Frontend.swift | 12 ++------ .../Subcommands/LintFormatOptions.swift | 28 ++++++++----------- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/Sources/swift-format/Frontend/Frontend.swift b/Sources/swift-format/Frontend/Frontend.swift index d9cbd4e94..247d682d3 100644 --- a/Sources/swift-format/Frontend/Frontend.swift +++ b/Sources/swift-format/Frontend/Frontend.swift @@ -125,15 +125,11 @@ class Frontend { return } - var selection: Selection = .infinite - if let offsets = lintFormatOptions.offsets { - selection = Selection(offsetRanges: offsets) - } let fileToProcess = FileToProcess( fileHandle: FileHandle.standardInput, url: URL(fileURLWithPath: lintFormatOptions.assumeFilename ?? ""), configuration: configuration, - selection: selection) + selection: Selection(offsetRanges: lintFormatOptions.offsets)) processFile(fileToProcess) } @@ -176,15 +172,11 @@ class Frontend { return nil } - var selection: Selection = .infinite - if let offsets = lintFormatOptions.offsets { - selection = Selection(offsetRanges: offsets) - } return FileToProcess( fileHandle: sourceFile, url: url, configuration: configuration, - selection: selection + selection: Selection(offsetRanges: lintFormatOptions.offsets) ) } diff --git a/Sources/swift-format/Subcommands/LintFormatOptions.swift b/Sources/swift-format/Subcommands/LintFormatOptions.swift index 85bcc7f38..098ad25d1 100644 --- a/Sources/swift-format/Subcommands/LintFormatOptions.swift +++ b/Sources/swift-format/Subcommands/LintFormatOptions.swift @@ -32,10 +32,10 @@ struct LintFormatOptions: ParsableArguments { @Option( name: .long, help: """ - A list of comma-separated "start:end" pairs specifying UTF-8 offsets of the ranges to format. + 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]? - + var offsets: [Range] = [] /// The filename for the source code when reading from standard input, to include in diagnostic /// messages. @@ -105,7 +105,7 @@ struct LintFormatOptions: ParsableArguments { throw ValidationError("'--assume-filename' is only valid when reading from stdin") } - if offsets?.isEmpty == false && paths.count > 1 { + if !offsets.isEmpty && paths.count > 1 { throw ValidationError("'--offsets' is only valid when processing a single file") } @@ -125,23 +125,19 @@ struct LintFormatOptions: ParsableArguments { } } -extension [Range] { +extension Range { public init?(argument: String) { - let pairs = argument.components(separatedBy: ",") - let ranges: [Range] = pairs.compactMap { - let pair = $0.components(separatedBy: ":") - if pair.count == 2, let start = Int(pair[0]), let end = Int(pair[1]), start <= end { - return start ..< end - } else { - return nil - } + 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 } - self = ranges } } #if compiler(>=6) -extension [Range] : @retroactive ExpressibleByArgument {} +extension Range : @retroactive ExpressibleByArgument {} #else -extension [Range] : ExpressibleByArgument {} +extension Range : ExpressibleByArgument {} #endif From 2541a149a583c964ea60f967891e8272373d89e1 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Fri, 14 Jun 2024 03:02:00 +0000 Subject: [PATCH 05/13] Fix `@_expose` attribute argument spacing Pretty-print the `@_expose` with the correct spacing. It was formatted as `@_expose(wasm,"foo")` instead of `@_expose(wasm, "foo")`. --- .../PrettyPrint/TokenStreamCreator.swift | 5 ++++ .../PrettyPrint/AttributeTests.swift | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift index 08f1aa4f0..95525d058 100644 --- a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift @@ -1802,6 +1802,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) 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) + } } From 95d85a2180cb1c5214735082d3810b92c86671fc Mon Sep 17 00:00:00 2001 From: Neil Jones Date: Tue, 18 Jun 2024 10:48:16 +0900 Subject: [PATCH 06/13] add support for riscv64 --- cmake/modules/SwiftSupport.cmake | 2 ++ 1 file changed, 2 insertions(+) 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() From ae0922db4e41cb18c2e055e563854bcc15629e09 Mon Sep 17 00:00:00 2001 From: Paris Date: Mon, 24 Jun 2024 11:17:00 -0700 Subject: [PATCH 07/13] Delete CODE_OF_CONDUCT.md deleting in favor of the organization wide coc; this file present means that the repo is opt-ing out of that --- CODE_OF_CONDUCT.md | 55 ---------------------------------------------- 1 file changed, 55 deletions(-) delete mode 100644 CODE_OF_CONDUCT.md 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. From 981c130a79862ba9c267fd689576d40ecaa4982d Mon Sep 17 00:00:00 2001 From: Paris Date: Mon, 24 Jun 2024 11:18:09 -0700 Subject: [PATCH 08/13] Delete CONTRIBUTING.md contains old language; moving towards a unified strategy with CONTRIBUTING files. --- CONTRIBUTING.md | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 CONTRIBUTING.md 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). From 1ca1b53cae94802a4442f27fcdc87299eedc6911 Mon Sep 17 00:00:00 2001 From: Paris Date: Mon, 24 Jun 2024 11:22:58 -0700 Subject: [PATCH 09/13] Update README.md adding in a contribution section from the removal of the contributing.md file --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 6b29d73eb..09a5144d4 100644 --- a/README.md +++ b/README.md @@ -251,3 +251,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/). From cef7eca0f466474c69e3b7420c59bbaf459aaff5 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Tue, 25 Jun 2024 04:36:28 -0700 Subject: [PATCH 10/13] Update links for repositories moved to the swiftlang org on GitHub --- CMakeLists.txt | 2 +- Documentation/PrettyPrinter.md | 2 +- Package.swift | 2 +- README.md | 8 ++++---- Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) 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/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 9a1995039..e1f123b36 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/apple/swift-markdown.git", from: "0.2.0"), - .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 6b29d73eb..cb05fc9a4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # 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) @@ -18,7 +18,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. @@ -34,7 +34,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. @@ -74,7 +74,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 diff --git a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift index 163457514..9f71a4fbf 100644 --- a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift @@ -853,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 From 56fa13b526dc344f079e999de6c31a21261e9c85 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Tue, 25 Jun 2024 04:55:33 -0700 Subject: [PATCH 11/13] Update README.md to mention that swift-format is included in Xcode 16 --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6b29d73eb..3706fa875 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,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 From a8ff3ca6045e1fbef3ef76ea73382028421fe450 Mon Sep 17 00:00:00 2001 From: Shawn Hyam Date: Tue, 25 Jun 2024 10:55:23 -0400 Subject: [PATCH 12/13] Split the PrettyPrint class into two pieces. This change breaks up the PrettyPrint class into one piece that is responsible for tracking the state of the pretty printing algorithm - processing the tokens, tracking the break stack, comma delimited region stack, etc.; and another piece that is responsible for assembling the output and tracking the state of the output - current line, column position, and indentation. --- .../PrettyPrint/Indent+Length.swift | 10 +- .../SwiftFormat/PrettyPrint/PrettyPrint.swift | 175 +++++------------- .../PrettyPrint/PrettyPrintBuffer.swift | 140 ++++++++++++++ 3 files changed, 194 insertions(+), 131 deletions(-) create mode 100644 Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift 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 0b4ff792a..5ab608a8a 100644 --- a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift @@ -76,12 +76,13 @@ public class PrettyPrinter { /// 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 - - private var outputBuffer: String = "" + private var disabledPosition: AbsolutePosition? = nil { + didSet { + outputBuffer.isEnabled = disabledPosition == nil + } + } - /// The number of spaces remaining on the current line. - private var spaceRemaining: Int + private var outputBuffer: PrettyPrintBuffer /// Keep track of the token lengths. private var lengths = [Int]() @@ -103,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] = [] @@ -111,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] = [] @@ -124,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 @@ -173,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. @@ -193,77 +189,9 @@ public class PrettyPrinter { 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) { - if disabledPosition == nil { - 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. @@ -285,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 @@ -306,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 (shouldBreak(length) || lastBreak), case .consistent = breaktype { forceBreakStack.append(true) } else { forceBreakStack.append(false) @@ -348,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 || shouldBreak(length) || mustBreak) let contributesContinuationIndent = currentLineIsContinuation || continuationBreakWillFire activeOpenBreaks.append( @@ -377,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 @@ -395,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 shouldBreak(1) { // 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. @@ -453,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 = shouldBreak(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. @@ -468,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 } } @@ -499,12 +427,12 @@ public class PrettyPrinter { } let suppressBreaking = isBreakingSuppressed && !overrideBreakingSuppressed - if !suppressBreaking && ((!isAtStartOfLine && length > spaceRemaining) || mustBreak) { + if !suppressBreaking && (shouldBreak(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 @@ -512,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 shouldBreak(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 { @@ -583,8 +505,7 @@ public class PrettyPrinter { let shouldWriteComma = whitespaceOnly ? hasTrailingComma : shouldHaveTrailingComma if shouldWriteComma { - write(",") - spaceRemaining -= 1 + outputBuffer.write(",") } case .enableFormatting(let enabledPosition): @@ -607,14 +528,7 @@ public class PrettyPrinter { } self.disabledPosition = nil - writeRaw(text) - if text.hasSuffix("\n") { - isAtStartOfLine = true - consecutiveNewlineCount = 1 - } else { - isAtStartOfLine = false - consecutiveNewlineCount = 0 - } + outputBuffer.writeVerbatimAfterEnablingFormatting(text) case .disableFormatting(let newPosition): assert(disabledPosition == nil) @@ -622,6 +536,11 @@ public class PrettyPrinter { } } + private func shouldBreak(_ length: Int) -> 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 @@ -748,7 +667,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. @@ -843,11 +762,11 @@ public class PrettyPrinter { /// 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..c975b1f7e --- /dev/null +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift @@ -0,0 +1,140 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +struct PrettyPrintBuffer { + let maximumBlankLines: Int + let tabWidth: Int + + 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 + + var currentIndentation: [Indent] + + 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)) + } +} From 415b8ea939267d18aa198ba73705a450d8846a4a Mon Sep 17 00:00:00 2001 From: Shawn Hyam Date: Wed, 26 Jun 2024 12:02:47 -0400 Subject: [PATCH 13/13] Add missing comments for new struct and its properties. --- .../SwiftFormat/PrettyPrint/PrettyPrint.swift | 18 ++++++++++-------- .../PrettyPrint/PrettyPrintBuffer.swift | 10 ++++++++++ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift index 5ab608a8a..ada574f1c 100644 --- a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift @@ -234,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 (shouldBreak(length) || lastBreak), case .consistent = breaktype { + if (!canFit(length) || lastBreak), case .consistent = breaktype { forceBreakStack.append(true) } else { forceBreakStack.append(false) @@ -276,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 - && (outputBuffer.isAtStartOfLine || shouldBreak(length) || mustBreak) + && (outputBuffer.isAtStartOfLine || !canFit(length) || mustBreak) let contributesContinuationIndent = currentLineIsContinuation || continuationBreakWillFire activeOpenBreaks.append( @@ -323,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 shouldBreak(1) { + } 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. @@ -387,7 +387,7 @@ public class PrettyPrinter { // 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 = shouldBreak(length) || 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. @@ -427,7 +427,7 @@ public class PrettyPrinter { } let suppressBreaking = isBreakingSuppressed && !overrideBreakingSuppressed - if !suppressBreaking && (shouldBreak(length) || mustBreak) { + if !suppressBreaking && (!canFit(length) || mustBreak) { currentLineIsContinuation = isContinuationIfBreakFires outputBuffer.writeNewlines(newline) lastBreak = true @@ -458,7 +458,7 @@ public class PrettyPrinter { lastBreak = false if wasEndOfLine { - if shouldBreak(comment.length) && !isBreakingSuppressed { + if !(canFit(comment.length) || isBreakingSuppressed) { diagnose(.moveEndOfLineComment, category: .endOfLineComment) } } @@ -536,9 +536,11 @@ public class PrettyPrinter { } } - private func shouldBreak(_ length: Int) -> Bool { + /// Indicates whether the current line can fit a string of the given length. If no length + /// is given, it indicates whether the current line can accomodate *any* text. + private func canFit(_ length: Int = 1) -> Bool { let spaceRemaining = configuration.lineLength - outputBuffer.column - return !outputBuffer.isAtStartOfLine && length > spaceRemaining + return outputBuffer.isAtStartOfLine || length <= spaceRemaining } /// Scan over the array of Tokens and calculate their lengths. diff --git a/Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift index c975b1f7e..02a2a7ac6 100644 --- a/Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift @@ -12,10 +12,18 @@ 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. @@ -37,8 +45,10 @@ struct PrettyPrintBuffer { /// 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) {