From eec64033cfcebb51c2b4c322ebb9233bade545fa Mon Sep 17 00:00:00 2001 From: Shawn Hyam Date: Tue, 28 May 2024 11:03:01 -0400 Subject: [PATCH 01/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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