diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 2b0a60355..000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,55 +0,0 @@ -# Code of Conduct -To be a truly great community, Swift.org needs to welcome developers from all walks of life, -with different backgrounds, and with a wide range of experience. A diverse and friendly -community will have more great ideas, more unique perspectives, and produce more great -code. We will work diligently to make the Swift community welcoming to everyone. - -To give clarity of what is expected of our members, Swift.org has adopted the code of conduct -defined by [contributor-covenant.org](https://www.contributor-covenant.org). This document is used across many open source -communities, and we think it articulates our values well. The full text is copied below: - -### Contributor Code of Conduct v1.3 -As contributors and maintainers of this project, and in the interest of fostering an open and -welcoming community, we pledge to respect all people who contribute through reporting -issues, posting feature requests, updating documentation, submitting pull requests or patches, -and other activities. - -We are committed to making participation in this project a harassment-free experience for -everyone, regardless of level of experience, gender, gender identity and expression, sexual -orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or -nationality. - -Examples of unacceptable behavior by participants include: -- The use of sexualized language or imagery -- Personal attacks -- Trolling or insulting/derogatory comments -- Public or private harassment -- Publishing other’s private information, such as physical or electronic addresses, without explicit permission -- Other unethical or unprofessional conduct - -Project maintainers have the right and responsibility to remove, edit, or reject comments, -commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of -Conduct, or to ban temporarily or permanently any contributor for other behaviors that they -deem inappropriate, threatening, offensive, or harmful. - -By adopting this Code of Conduct, project maintainers commit themselves to fairly and -consistently applying these principles to every aspect of managing this project. Project -maintainers who do not follow or enforce the Code of Conduct may be permanently removed -from the project team. - -This code of conduct applies both within project spaces and in public spaces when an -individual is representing the project or its community. - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by -contacting a project maintainer at [conduct@swift.org](mailto:conduct@swift.org). All complaints will be reviewed and -investigated and will result in a response that is deemed necessary and appropriate to the -circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter -of an incident. - -*This policy is adapted from the Contributor Code of Conduct [version 1.3.0](http://contributor-covenant.org/version/1/3/0/).* - -### Reporting -A working group of community members is committed to promptly addressing any [reported -issues](mailto:conduct@swift.org). Working group members are volunteers appointed by the project lead, with a -preference for individuals with varied backgrounds and perspectives. Membership is expected -to change regularly, and may grow or shrink. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 9f01e1f3b..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,11 +0,0 @@ -By submitting a pull request, you represent that you have the right to license -your contribution to Apple and the community, and agree by submitting the patch -that your contributions are licensed under the [Swift -license](https://swift.org/LICENSE.txt). - ---- - -Before submitting the pull request, please make sure you have [tested your -changes](https://github.com/apple/swift/blob/main/docs/ContinuousIntegration.md) -and that they follow the Swift project [guidelines for contributing -code](https://swift.org/contributing/#contributing-code). diff --git a/README.md b/README.md index cb05fc9a4..6f08b6ec3 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 @@ -251,3 +254,24 @@ been merged into `main`. If you are interested in developing `swift-format`, there is additional documentation about that [here](Documentation/Development.md). + +## Contributing + +Contributions to Swift are welcomed and encouraged! Please see the +[Contributing to Swift guide](https://swift.org/contributing/). + +Before submitting the pull request, please make sure you have [tested your + changes](https://github.com/apple/swift/blob/main/docs/ContinuousIntegration.md) + and that they follow the Swift project [guidelines for contributing + code](https://swift.org/contributing/#contributing-code). + +To be a truly great community, [Swift.org](https://swift.org/) needs to welcome +developers from all walks of life, with different backgrounds, and with a wide +range of experience. A diverse and friendly community will have more great +ideas, more unique perspectives, and produce more great code. We will work +diligently to make the Swift community welcoming to everyone. + +To give clarity of what is expected of our members, Swift has adopted the +code of conduct defined by the Contributor Covenant. This document is used +across many open source communities, and we think it articulates our values +well. For more, see the [Code of Conduct](https://swift.org/code-of-conduct/). diff --git a/Sources/SwiftFormat/API/Selection.swift b/Sources/SwiftFormat/API/Selection.swift new file mode 100644 index 000000000..9ea599db3 --- /dev/null +++ b/Sources/SwiftFormat/API/Selection.swift @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Foundation +import SwiftSyntax + +/// The selection as given on the command line - an array of offets and lengths +public enum Selection { + case infinite + case ranges([Range]) + + /// Create a selection from an array of utf8 ranges. An empty array means an infinite selection. + public init(offsetRanges: [Range]) { + if offsetRanges.isEmpty { + self = .infinite + } else { + let ranges = offsetRanges.map { + AbsolutePosition(utf8Offset: $0.lowerBound) ..< AbsolutePosition(utf8Offset: $0.upperBound) + } + self = .ranges(ranges) + } + } + + public func contains(_ position: AbsolutePosition) -> Bool { + switch self { + case .infinite: + return true + case .ranges(let ranges): + return ranges.contains { $0.contains(position) } + } + } + + public func overlapsOrTouches(_ range: Range) -> Bool { + switch self { + case .infinite: + return true + case .ranges(let ranges): + return ranges.contains { $0.overlapsOrTouches(range) } + } + } +} + + +public extension Syntax { + /// - Returns: `true` if the node is _completely_ inside any range in the selection + func isInsideSelection(_ selection: Selection) -> Bool { + switch selection { + case .infinite: + return true + case .ranges(let ranges): + return ranges.contains { return $0.lowerBound <= position && endPosition <= $0.upperBound } + } + } +} diff --git a/Sources/SwiftFormat/API/SwiftFormatter.swift b/Sources/SwiftFormat/API/SwiftFormatter.swift index 9230bdd8f..e91030b3c 100644 --- a/Sources/SwiftFormat/API/SwiftFormatter.swift +++ b/Sources/SwiftFormat/API/SwiftFormatter.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -70,6 +70,7 @@ public final class SwiftFormatter { try format( source: String(contentsOf: url, encoding: .utf8), assumingFileURL: url, + selection: .infinite, to: &outputStream, parsingDiagnosticHandler: parsingDiagnosticHandler) } @@ -86,6 +87,7 @@ public final class SwiftFormatter { /// - url: A file URL denoting the filename/path that should be assumed for this syntax tree, /// which is associated with any diagnostics emitted during formatting. If this is nil, a /// dummy value will be used. + /// - selection: The ranges to format /// - outputStream: A value conforming to `TextOutputStream` to which the formatted output will /// be written. /// - parsingDiagnosticHandler: An optional callback that will be notified if there are any @@ -94,6 +96,7 @@ public final class SwiftFormatter { public func format( source: String, assumingFileURL url: URL?, + selection: Selection, to outputStream: inout Output, parsingDiagnosticHandler: ((Diagnostic, SourceLocation) -> Void)? = nil ) throws { @@ -108,8 +111,8 @@ public final class SwiftFormatter { assumingFileURL: url, parsingDiagnosticHandler: parsingDiagnosticHandler) try format( - syntax: sourceFile, operatorTable: .standardOperators, assumingFileURL: url, source: source, - to: &outputStream) + syntax: sourceFile, source: source, operatorTable: .standardOperators, assumingFileURL: url, + selection: selection, to: &outputStream) } /// Formats the given Swift syntax tree and writes the result to an output stream. @@ -122,32 +125,26 @@ public final class SwiftFormatter { /// /// - Parameters: /// - syntax: The Swift syntax tree to be converted to source code and formatted. + /// - source: The original Swift source code used to build the syntax tree. /// - operatorTable: The table that defines the operators and their precedence relationships. /// This must be the same operator table that was used to fold the expressions in the `syntax` /// argument. /// - url: A file URL denoting the filename/path that should be assumed for this syntax tree, /// which is associated with any diagnostics emitted during formatting. If this is nil, a /// dummy value will be used. + /// - selection: The ranges to format /// - outputStream: A value conforming to `TextOutputStream` to which the formatted output will /// be written. /// - Throws: If an unrecoverable error occurs when formatting the code. public func format( - syntax: SourceFileSyntax, operatorTable: OperatorTable, assumingFileURL url: URL?, - to outputStream: inout Output - ) throws { - try format( - syntax: syntax, operatorTable: operatorTable, assumingFileURL: url, source: nil, - to: &outputStream) - } - - private func format( - syntax: SourceFileSyntax, operatorTable: OperatorTable, - assumingFileURL url: URL?, source: String?, to outputStream: inout Output + syntax: SourceFileSyntax, source: String, operatorTable: OperatorTable, + assumingFileURL url: URL?, selection: Selection, to outputStream: inout Output ) throws { let assumedURL = url ?? URL(fileURLWithPath: "source") let context = Context( configuration: configuration, operatorTable: operatorTable, findingConsumer: findingConsumer, - fileURL: assumedURL, sourceFileSyntax: syntax, source: source, ruleNameCache: ruleNameCache) + fileURL: assumedURL, selection: selection, sourceFileSyntax: syntax, source: source, + ruleNameCache: ruleNameCache) let pipeline = FormatPipeline(context: context) let transformedSyntax = pipeline.rewrite(Syntax(syntax)) @@ -158,6 +155,7 @@ public final class SwiftFormatter { let printer = PrettyPrinter( context: context, + source: source, node: transformedSyntax, printTokenStream: debugOptions.contains(.dumpTokenStream), whitespaceOnly: false) diff --git a/Sources/SwiftFormat/API/SwiftLinter.swift b/Sources/SwiftFormat/API/SwiftLinter.swift index 4806f19df..79568e2cb 100644 --- a/Sources/SwiftFormat/API/SwiftLinter.swift +++ b/Sources/SwiftFormat/API/SwiftLinter.swift @@ -119,17 +119,18 @@ public final class SwiftLinter { /// - Throws: If an unrecoverable error occurs when formatting the code. public func lint( syntax: SourceFileSyntax, + source: String, operatorTable: OperatorTable, assumingFileURL url: URL ) throws { - try lint(syntax: syntax, operatorTable: operatorTable, assumingFileURL: url, source: nil) + try lint(syntax: syntax, operatorTable: operatorTable, assumingFileURL: url, source: source) } private func lint( syntax: SourceFileSyntax, operatorTable: OperatorTable, assumingFileURL url: URL, - source: String? + source: String ) throws { let context = Context( configuration: configuration, operatorTable: operatorTable, findingConsumer: findingConsumer, @@ -145,6 +146,7 @@ public final class SwiftLinter { // pretty-printer. let printer = PrettyPrinter( context: context, + source: source, node: Syntax(syntax), printTokenStream: debugOptions.contains(.dumpTokenStream), whitespaceOnly: true) diff --git a/Sources/SwiftFormat/CMakeLists.txt b/Sources/SwiftFormat/CMakeLists.txt index 4ce86a341..cb3998722 100644 --- a/Sources/SwiftFormat/CMakeLists.txt +++ b/Sources/SwiftFormat/CMakeLists.txt @@ -14,6 +14,7 @@ add_library(SwiftFormat API/Finding.swift API/FindingCategorizing.swift API/Indent.swift + API/Selection.swift API/SwiftFormatError.swift API/SwiftFormatter.swift API/SwiftLinter.swift diff --git a/Sources/SwiftFormat/Core/Context.swift b/Sources/SwiftFormat/Core/Context.swift index 29e69b0dc..2bbb900ac 100644 --- a/Sources/SwiftFormat/Core/Context.swift +++ b/Sources/SwiftFormat/Core/Context.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -39,6 +39,9 @@ public final class Context { /// The configuration for this run of the pipeline, provided by a configuration JSON file. let configuration: Configuration + /// The selection to process + let selection: Selection + /// Defines the operators and their precedence relationships that were used during parsing. let operatorTable: OperatorTable @@ -66,6 +69,7 @@ public final class Context { operatorTable: OperatorTable, findingConsumer: ((Finding) -> Void)?, fileURL: URL, + selection: Selection = .infinite, sourceFileSyntax: SourceFileSyntax, source: String? = nil, ruleNameCache: [ObjectIdentifier: String] @@ -74,6 +78,7 @@ public final class Context { self.operatorTable = operatorTable self.findingEmitter = FindingEmitter(consumer: findingConsumer) self.fileURL = fileURL + self.selection = selection self.importsXCTest = .notDetermined let tree = source.map { Parser.parse(source: $0) } ?? sourceFileSyntax self.sourceLocationConverter = @@ -86,8 +91,10 @@ public final class Context { } /// Given a rule's name and the node it is examining, determine if the rule is disabled at this - /// location or not. - func isRuleEnabled(_ rule: R.Type, node: Syntax) -> Bool { + /// location or not. Also makes sure the entire node is contained inside any selection. + func shouldFormat(_ rule: R.Type, node: Syntax) -> Bool { + guard node.isInsideSelection(selection) else { return false } + let loc = node.startLocation(converter: self.sourceLocationConverter) assert( diff --git a/Sources/SwiftFormat/Core/DocumentationCommentText.swift b/Sources/SwiftFormat/Core/DocumentationCommentText.swift index 44ef1a61a..32dd82a18 100644 --- a/Sources/SwiftFormat/Core/DocumentationCommentText.swift +++ b/Sources/SwiftFormat/Core/DocumentationCommentText.swift @@ -67,24 +67,7 @@ public struct DocumentationCommentText { // comment. We have to copy it into an array since `Trivia` doesn't support bidirectional // indexing. let triviaArray = Array(trivia) - let commentStartIndex: Array.Index - if - let lastNonDocCommentIndex = triviaArray.lastIndex(where: { - switch $0 { - case .docBlockComment, .docLineComment, - .newlines(1), .carriageReturns(1), .carriageReturnLineFeeds(1), - .spaces, .tabs: - return false - default: - return true - } - }), - lastNonDocCommentIndex != trivia.endIndex - { - commentStartIndex = triviaArray.index(after: lastNonDocCommentIndex) - } else { - commentStartIndex = triviaArray.startIndex - } + let commentStartIndex = findCommentStartIndex(triviaArray) // Determine the indentation level of the first line of the comment. This is used to adjust // block comments, whose text spans multiple lines. @@ -216,3 +199,37 @@ private func asciiArtLength(of string: Substring, leadingSpaces: Int) -> Int { } return 0 } + +/// Returns the start index of the earliest comment in the Trivia if we work backwards and +/// skip through comments, newlines, and whitespace. Then we advance a bit forward to be sure +/// the returned index is actually a comment and not whitespace. +private func findCommentStartIndex(_ triviaArray: Array) -> Array.Index { + func firstCommentIndex(_ slice: ArraySlice) -> Array.Index { + return slice.firstIndex(where: { + switch $0 { + case .docLineComment, .docBlockComment: + return true + default: + return false + } + }) ?? slice.endIndex + } + + if + let lastNonDocCommentIndex = triviaArray.lastIndex(where: { + switch $0 { + case .docBlockComment, .docLineComment, + .newlines(1), .carriageReturns(1), .carriageReturnLineFeeds(1), + .spaces, .tabs: + return false + default: + return true + } + }) + { + let nextIndex = triviaArray.index(after: lastNonDocCommentIndex) + return firstCommentIndex(triviaArray[nextIndex...]) + } else { + return firstCommentIndex(triviaArray[...]) + } +} diff --git a/Sources/SwiftFormat/Core/LintPipeline.swift b/Sources/SwiftFormat/Core/LintPipeline.swift index 3eb10072d..58d9f6d13 100644 --- a/Sources/SwiftFormat/Core/LintPipeline.swift +++ b/Sources/SwiftFormat/Core/LintPipeline.swift @@ -28,7 +28,7 @@ extension LintPipeline { func visitIfEnabled( _ visitor: (Rule) -> (Node) -> SyntaxVisitorContinueKind, for node: Node ) { - guard context.isRuleEnabled(Rule.self, node: Syntax(node)) else { return } + guard context.shouldFormat(Rule.self, node: Syntax(node)) else { return } let ruleId = ObjectIdentifier(Rule.self) guard self.shouldSkipChildren[ruleId] == nil else { return } let rule = self.rule(Rule.self) @@ -54,7 +54,7 @@ extension LintPipeline { // more importantly because the `visit` methods return protocol refinements of `Syntax` that // cannot currently be expressed as constraints without duplicating this function for each of // them individually. - guard context.isRuleEnabled(Rule.self, node: Syntax(node)) else { return } + guard context.shouldFormat(Rule.self, node: Syntax(node)) else { return } guard self.shouldSkipChildren[ObjectIdentifier(Rule.self)] == nil else { return } let rule = self.rule(Rule.self) _ = visitor(rule)(node) diff --git a/Sources/SwiftFormat/Core/SyntaxFormatRule.swift b/Sources/SwiftFormat/Core/SyntaxFormatRule.swift index 767e59fcf..92fc7c835 100644 --- a/Sources/SwiftFormat/Core/SyntaxFormatRule.swift +++ b/Sources/SwiftFormat/Core/SyntaxFormatRule.swift @@ -32,7 +32,7 @@ public class SyntaxFormatRule: SyntaxRewriter, Rule { public override func visitAny(_ node: Syntax) -> Syntax? { // If the rule is not enabled, then return the node unmodified; otherwise, returning nil tells // SwiftSyntax to continue with the standard dispatch. - guard context.isRuleEnabled(type(of: self), node: node) else { return node } + guard context.shouldFormat(type(of: self), node: node) else { return node } return nil } } diff --git a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift index 1201f84c2..0b4ff792a 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,17 @@ 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 + private var outputBuffer: String = "" /// The number of spaces remaining on the current line. @@ -172,11 +184,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 @@ -187,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 @@ -241,7 +258,7 @@ public class PrettyPrinter { writeRaw(currentIndentation.indentation()) spaceRemaining = maxLineLength - currentIndentation.length(in: configuration) isAtStartOfLine = false - } else if pendingSpaces > 0 { + } else if pendingSpaces > 0 { writeRaw(String(repeating: " ", count: pendingSpaces)) } writeRaw(text) @@ -569,6 +586,39 @@ public class PrettyPrinter { write(",") spaceRemaining -= 1 } + + case .enableFormatting(let enabledPosition): + guard let disabledPosition else { + // if we're not disabled, we ignore the token + break + } + let start = source.utf8.index(source.utf8.startIndex, offsetBy: disabledPosition.utf8Offset) + let end: String.Index + if let enabledPosition { + end = source.utf8.index(source.utf8.startIndex, offsetBy: enabledPosition.utf8Offset) + } else { + end = source.endIndex + } + var text = String(source[start..() - 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 } @@ -1802,6 +1818,11 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { return .visitChildren } + override func visit(_ node: ExposeAttributeArgumentsSyntax) -> SyntaxVisitorContinueKind { + after(node.comma, tokens: .break(.same, size: 1)) + return .visitChildren + } + override func visit(_ node: AvailabilityLabeledArgumentSyntax) -> SyntaxVisitorContinueKind { before(node.label, tokens: .open) @@ -2719,11 +2740,17 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { extractLeadingTrivia(token) closeScopeTokens.forEach(appendToken) + generateEnableFormattingIfNecessary( + token.positionAfterSkippingLeadingTrivia ..< token.endPositionBeforeTrailingTrivia + ) + if !ignoredTokens.contains(token) { // Otherwise, it's just a regular token, so add the text as-is. appendToken(.syntax(token.presence == .present ? token.text : "")) } + generateDisableFormattingIfNecessary(token.endPositionBeforeTrailingTrivia) + appendTrailingTrivia(token) appendAfterTokensAndTrailingComments(token) @@ -2731,6 +2758,22 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { return .skipChildren } + private func generateEnableFormattingIfNecessary(_ range: Range) { + if case .infinite = selection { return } + if !isInsideSelection && selection.overlapsOrTouches(range) { + appendToken(.enableFormatting(range.lowerBound)) + isInsideSelection = true + } + } + + private func generateDisableFormattingIfNecessary(_ position: AbsolutePosition) { + if case .infinite = selection { return } + if isInsideSelection && !selection.contains(position) { + appendToken(.disableFormatting(position)) + isInsideSelection = false + } + } + /// Appends the before-tokens of the given syntax token to the token stream. private func appendBeforeTokens(_ token: TokenSyntax) { if let before = beforeMap.removeValue(forKey: token) { @@ -3194,11 +3237,14 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { private func extractLeadingTrivia(_ token: TokenSyntax) { var isStartOfFile: Bool let trivia: Trivia + var position = token.position if let previousToken = token.previousToken(viewMode: .sourceAccurate) { isStartOfFile = false // Find the first non-whitespace in the previous token's trailing and peel those off. let (_, prevTrailingComments) = partitionTrailingTrivia(previousToken.trailingTrivia) - trivia = Trivia(pieces: prevTrailingComments) + token.leadingTrivia + let prevTrivia = Trivia(pieces: prevTrailingComments) + trivia = prevTrivia + token.leadingTrivia + position -= prevTrivia.sourceLength } else { isStartOfFile = true trivia = token.leadingTrivia @@ -3229,7 +3275,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { switch piece { case .lineComment(let text): if index > 0 || isStartOfFile { + generateEnableFormattingIfNecessary(position ..< position + piece.sourceLength) appendToken(.comment(Comment(kind: .line, text: text), wasEndOfLine: false)) + generateDisableFormattingIfNecessary(position + piece.sourceLength) appendNewlines(.soft) isStartOfFile = false } @@ -3237,7 +3285,9 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { case .blockComment(let text): if index > 0 || isStartOfFile { + generateEnableFormattingIfNecessary(position ..< position + piece.sourceLength) appendToken(.comment(Comment(kind: .block, text: text), wasEndOfLine: false)) + generateDisableFormattingIfNecessary(position + piece.sourceLength) // There is always a break after the comment to allow a discretionary newline after it. var breakSize = 0 if index + 1 < trivia.endIndex { @@ -3252,13 +3302,17 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { requiresNextNewline = false case .docLineComment(let text): + generateEnableFormattingIfNecessary(position ..< position + piece.sourceLength) appendToken(.comment(Comment(kind: .docLine, text: text), wasEndOfLine: false)) + generateDisableFormattingIfNecessary(position + piece.sourceLength) appendNewlines(.soft) isStartOfFile = false requiresNextNewline = true case .docBlockComment(let text): + generateEnableFormattingIfNecessary(position ..< position + piece.sourceLength) appendToken(.comment(Comment(kind: .docBlock, text: text), wasEndOfLine: false)) + generateDisableFormattingIfNecessary(position + piece.sourceLength) appendNewlines(.soft) isStartOfFile = false requiresNextNewline = false @@ -3297,6 +3351,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { default: break } + position += piece.sourceLength } } @@ -3432,7 +3487,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { case .break: lastBreakIndex = tokens.endIndex canMergeNewlinesIntoLastBreak = true - case .open, .printerControl, .contextualBreakingStart: + case .open, .printerControl, .contextualBreakingStart, .enableFormatting, .disableFormatting: break default: canMergeNewlinesIntoLastBreak = false @@ -3997,10 +4052,17 @@ private func isNestedInPostfixIfConfig(node: Syntax) -> Bool { extension Syntax { /// Creates a pretty-printable token stream for the provided Syntax node. - func makeTokenStream(configuration: Configuration, operatorTable: OperatorTable) -> [Token] { - let commentsMoved = CommentMovingRewriter().rewrite(self) - return TokenStreamCreator(configuration: configuration, operatorTable: operatorTable) - .makeStream(from: commentsMoved) + func makeTokenStream( + configuration: Configuration, + selection: Selection, + operatorTable: OperatorTable + ) -> [Token] { + let commentsMoved = CommentMovingRewriter(selection: selection).rewrite(self) + return TokenStreamCreator( + configuration: configuration, + selection: selection, + operatorTable: operatorTable + ).makeStream(from: commentsMoved) } } @@ -4010,6 +4072,12 @@ extension Syntax { /// For example, comments after binary operators are relocated to be before the operator, which /// results in fewer line breaks with the comment closer to the relevant tokens. class CommentMovingRewriter: SyntaxRewriter { + init(selection: Selection = .infinite) { + self.selection = selection + } + + private let selection: Selection + override func visit(_ node: SourceFileSyntax) -> SourceFileSyntax { if shouldFormatterIgnore(file: node) { return node @@ -4018,14 +4086,14 @@ class CommentMovingRewriter: SyntaxRewriter { } override func visit(_ node: CodeBlockItemSyntax) -> CodeBlockItemSyntax { - if shouldFormatterIgnore(node: Syntax(node)) { + if shouldFormatterIgnore(node: Syntax(node)) || !Syntax(node).isInsideSelection(selection) { return node } return super.visit(node) } override func visit(_ node: MemberBlockItemSyntax) -> MemberBlockItemSyntax { - if shouldFormatterIgnore(node: Syntax(node)) { + if shouldFormatterIgnore(node: Syntax(node)) || !Syntax(node).isInsideSelection(selection) { return node } return super.visit(node) diff --git a/Sources/SwiftFormat/Rules/OrderedImports.swift b/Sources/SwiftFormat/Rules/OrderedImports.swift index 292bbd472..6d2475a1b 100644 --- a/Sources/SwiftFormat/Rules/OrderedImports.swift +++ b/Sources/SwiftFormat/Rules/OrderedImports.swift @@ -310,7 +310,7 @@ fileprivate func generateLines(codeBlockItemList: CodeBlockItemListSyntax, conte if currentLine.syntaxNode != nil { appendNewLine() } - let sortable = context.isRuleEnabled(OrderedImports.self, node: Syntax(block)) + let sortable = context.shouldFormat(OrderedImports.self, node: Syntax(block)) var blockWithoutTrailingTrivia = block blockWithoutTrailingTrivia.trailingTrivia = [] currentLine.syntaxNode = .importCodeBlock(blockWithoutTrailingTrivia, sortable: sortable) diff --git a/Sources/_SwiftFormatTestSupport/DiagnosingTestCase.swift b/Sources/_SwiftFormatTestSupport/DiagnosingTestCase.swift index f7a9b25a8..1c8054d23 100644 --- a/Sources/_SwiftFormatTestSupport/DiagnosingTestCase.swift +++ b/Sources/_SwiftFormatTestSupport/DiagnosingTestCase.swift @@ -15,6 +15,7 @@ open class DiagnosingTestCase: XCTestCase { public func makeContext( sourceFileSyntax: SourceFileSyntax, configuration: Configuration? = nil, + selection: Selection, findingConsumer: @escaping (Finding) -> Void ) -> Context { let context = Context( @@ -22,6 +23,7 @@ open class DiagnosingTestCase: XCTestCase { operatorTable: .standardOperators, findingConsumer: findingConsumer, fileURL: URL(fileURLWithPath: "/tmp/test.swift"), + selection: selection, sourceFileSyntax: sourceFileSyntax, ruleNameCache: ruleNameCache) return context diff --git a/Sources/_SwiftFormatTestSupport/MarkedText.swift b/Sources/_SwiftFormatTestSupport/MarkedText.swift index e43c8ccf8..071a7540a 100644 --- a/Sources/_SwiftFormatTestSupport/MarkedText.swift +++ b/Sources/_SwiftFormatTestSupport/MarkedText.swift @@ -10,6 +10,9 @@ // //===----------------------------------------------------------------------===// +import SwiftSyntax +import SwiftFormat + /// Encapsulates the locations of emoji markers extracted from source text. public struct MarkedText { /// A mapping from marker names to the UTF-8 offset where the marker was found in the string. @@ -18,23 +21,35 @@ public struct MarkedText { /// The text with all markers removed. public let textWithoutMarkers: String + /// If the marked text contains "⏩" and "⏪", they're used to create a selection + public var selection: Selection + /// Creates a new `MarkedText` value by extracting emoji markers from the given text. public init(textWithMarkers markedText: String) { var text = "" var markers = [String: Int]() var lastIndex = markedText.startIndex + var offsets = [Range]() + var lastRangeStart = 0 for marker in findMarkedRanges(in: markedText) { text += markedText[lastIndex.."), - configuration: configuration) + configuration: configuration, + selection: Selection(offsetRanges: lintFormatOptions.offsets)) processFile(fileToProcess) } @@ -162,7 +172,12 @@ class Frontend { return nil } - return FileToProcess(fileHandle: sourceFile, url: url, configuration: configuration) + return FileToProcess( + fileHandle: sourceFile, + url: url, + configuration: configuration, + selection: Selection(offsetRanges: lintFormatOptions.offsets) + ) } /// Returns the configuration that applies to the given `.swift` source file, when an explicit diff --git a/Sources/swift-format/Subcommands/LintFormatOptions.swift b/Sources/swift-format/Subcommands/LintFormatOptions.swift index 4e98d1e14..098ad25d1 100644 --- a/Sources/swift-format/Subcommands/LintFormatOptions.swift +++ b/Sources/swift-format/Subcommands/LintFormatOptions.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -26,6 +26,17 @@ struct LintFormatOptions: ParsableArguments { """) var configuration: String? + /// A list of comma-separated "start:end" pairs specifying UTF-8 offsets of the ranges to format. + /// + /// If not specified, the whole file will be formatted. + @Option( + name: .long, + help: """ + A "start:end" pair specifying UTF-8 offsets of the range to format. Multiple ranges can be + formatted by specifying several --offsets arguments. + """) + var offsets: [Range] = [] + /// The filename for the source code when reading from standard input, to include in diagnostic /// messages. /// @@ -94,6 +105,10 @@ struct LintFormatOptions: ParsableArguments { throw ValidationError("'--assume-filename' is only valid when reading from stdin") } + if !offsets.isEmpty && paths.count > 1 { + throw ValidationError("'--offsets' is only valid when processing a single file") + } + if !paths.isEmpty && !recursive { for path in paths { var isDir: ObjCBool = false @@ -109,3 +124,20 @@ struct LintFormatOptions: ParsableArguments { } } } + +extension Range { + public init?(argument: String) { + let pair = argument.components(separatedBy: ":") + if pair.count == 2, let start = Int(pair[0]), let end = Int(pair[1]), start <= end { + self = start ..< end + } else { + return nil + } + } +} + +#if compiler(>=6) +extension Range : @retroactive ExpressibleByArgument {} +#else +extension Range : ExpressibleByArgument {} +#endif diff --git a/Tests/SwiftFormatPerformanceTests/WhitespaceLinterPerformanceTests.swift b/Tests/SwiftFormatPerformanceTests/WhitespaceLinterPerformanceTests.swift index 938fdad49..960033604 100644 --- a/Tests/SwiftFormatPerformanceTests/WhitespaceLinterPerformanceTests.swift +++ b/Tests/SwiftFormatPerformanceTests/WhitespaceLinterPerformanceTests.swift @@ -58,7 +58,8 @@ final class WhitespaceLinterPerformanceTests: DiagnosingTestCase { /// - expected: The formatted text. private func performWhitespaceLint(input: String, expected: String) { let sourceFileSyntax = Parser.parse(source: input) - let context = makeContext(sourceFileSyntax: sourceFileSyntax, findingConsumer: { _ in }) + let context = makeContext(sourceFileSyntax: sourceFileSyntax, selection: .infinite, + findingConsumer: { _ in }) let linter = WhitespaceLinter(user: input, formatted: expected, context: context) linter.lint() } diff --git a/Tests/SwiftFormatTests/Core/DocumentationCommentTextTests.swift b/Tests/SwiftFormatTests/Core/DocumentationCommentTextTests.swift index adda6e4f1..4a1f8302f 100644 --- a/Tests/SwiftFormatTests/Core/DocumentationCommentTextTests.swift +++ b/Tests/SwiftFormatTests/Core/DocumentationCommentTextTests.swift @@ -54,7 +54,25 @@ final class DocumentationCommentTextTests: XCTestCase { """ ) } - + + func testIndentedDocBlockCommentWithASCIIArt() throws { + let decl: DeclSyntax = """ + /** + * A simple doc comment. + */ + func f() {} + """ + let commentText = try XCTUnwrap(DocumentationCommentText(extractedFrom: decl.leadingTrivia)) + XCTAssertEqual(commentText.introducer, .block) + XCTAssertEqual( + commentText.text, + """ + A simple doc comment. + + """ + ) + } + func testDocBlockCommentWithoutASCIIArt() throws { let decl: DeclSyntax = """ /** diff --git a/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift b/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift index 3031fc31b..3ff5db02d 100644 --- a/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift +++ b/Tests/SwiftFormatTests/PrettyPrint/AttributeTests.swift @@ -444,4 +444,28 @@ final class AttributeTests: PrettyPrintTestCase { assertPrettyPrintEqual(input: input, expected: expected, linelength: 100) } + + func testAttributeParamSpacingInExpose() { + let input = + """ + @_expose( wasm , "foo" ) + func f() {} + + @_expose( Cxx , "bar") + func b() {} + + """ + + let expected = + """ + @_expose(wasm, "foo") + func f() {} + + @_expose(Cxx, "bar") + func b() {} + + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 100) + } } diff --git a/Tests/SwiftFormatTests/PrettyPrint/IgnoreNodeTests.swift b/Tests/SwiftFormatTests/PrettyPrint/IgnoreNodeTests.swift index 4e51f4de5..3153ccd80 100644 --- a/Tests/SwiftFormatTests/PrettyPrint/IgnoreNodeTests.swift +++ b/Tests/SwiftFormatTests/PrettyPrint/IgnoreNodeTests.swift @@ -1,5 +1,5 @@ final class IgnoreNodeTests: PrettyPrintTestCase { - func atestIgnoreCodeBlockListItems() { + func testIgnoreCodeBlockListItems() { let input = """ x = 4 + 5 // This comment stays here. diff --git a/Tests/SwiftFormatTests/PrettyPrint/PrettyPrintTestCase.swift b/Tests/SwiftFormatTests/PrettyPrint/PrettyPrintTestCase.swift index 3aa54af95..eaa33ac3a 100644 --- a/Tests/SwiftFormatTests/PrettyPrint/PrettyPrintTestCase.swift +++ b/Tests/SwiftFormatTests/PrettyPrint/PrettyPrintTestCase.swift @@ -43,6 +43,7 @@ class PrettyPrintTestCase: DiagnosingTestCase { let (formatted, context) = prettyPrintedSource( markedInput.textWithoutMarkers, configuration: configuration, + selection: markedInput.selection, whitespaceOnly: whitespaceOnly, findingConsumer: { emittedFindings.append($0) }) assertStringsEqualWithDiff( @@ -64,14 +65,18 @@ class PrettyPrintTestCase: DiagnosingTestCase { // Idempotency check: Running the formatter multiple times should not change the outcome. // Assert that running the formatter again on the previous result keeps it the same. - let (reformatted, _) = prettyPrintedSource( - formatted, - configuration: configuration, - whitespaceOnly: whitespaceOnly, - findingConsumer: { _ in } // Ignore findings during the idempotence check. - ) - assertStringsEqualWithDiff( - reformatted, formatted, "Pretty printer is not idempotent", file: file, line: line) + // But if we have ranges, they aren't going to be valid for the formatted text. + if case .infinite = markedInput.selection { + let (reformatted, _) = prettyPrintedSource( + formatted, + configuration: configuration, + selection: markedInput.selection, + whitespaceOnly: whitespaceOnly, + findingConsumer: { _ in } // Ignore findings during the idempotence check. + ) + assertStringsEqualWithDiff( + reformatted, formatted, "Pretty printer is not idempotent", file: file, line: line) + } } /// Returns the given source code reformatted with the pretty printer. @@ -86,6 +91,7 @@ class PrettyPrintTestCase: DiagnosingTestCase { private func prettyPrintedSource( _ source: String, configuration: Configuration, + selection: Selection, whitespaceOnly: Bool, findingConsumer: @escaping (Finding) -> Void ) -> (String, Context) { @@ -96,9 +102,11 @@ class PrettyPrintTestCase: DiagnosingTestCase { let context = makeContext( sourceFileSyntax: sourceFileSyntax, configuration: configuration, + selection: selection, findingConsumer: findingConsumer) let printer = PrettyPrinter( context: context, + source: source, node: Syntax(sourceFileSyntax), printTokenStream: false, whitespaceOnly: whitespaceOnly) diff --git a/Tests/SwiftFormatTests/PrettyPrint/SelectionTests.swift b/Tests/SwiftFormatTests/PrettyPrint/SelectionTests.swift new file mode 100644 index 000000000..fb95b2a6e --- /dev/null +++ b/Tests/SwiftFormatTests/PrettyPrint/SelectionTests.swift @@ -0,0 +1,395 @@ +import SwiftFormat +import XCTest + +final class SelectionTests: PrettyPrintTestCase { + func testSelectAll() { + let input = + """ + ⏩func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + } + }⏪ + """ + + let expected = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + } + } + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + func testSelectComment() { + let input = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + ⏩// do stuff⏪ + } + } + """ + + let expected = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + } + } + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + func testInsertionPointBeforeComment() { + let input = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + ⏩⏪// do stuff + } + } + """ + + let expected = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + } + } + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + func testSpacesInline() { + let input = + """ + func foo() { + if let SomeReallyLongVar ⏩ = ⏪Some.More.Stuff(), let a = myfunc() { + // do stuff + } + } + """ + + let expected = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + } + } + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + func testSpacesFullLine() { + let input = + """ + func foo() { + ⏩if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() {⏪ + // do stuff + } + } + """ + + let expected = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + } + } + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + func testWrapInline() { + let input = + """ + func foo() { + if let SomeReallyLongVar = ⏩Some.More.Stuff(), let a = myfunc()⏪ { + // do stuff + } + } + """ + + let expected = + """ + func foo() { + if let SomeReallyLongVar = Some.More + .Stuff(), let a = myfunc() { + // do stuff + } + } + """ + + // The line length ends on the last paren of .Stuff() + assertPrettyPrintEqual(input: input, expected: expected, linelength: 44) + } + + func testCommentsOnly() { + let input = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + ⏩// do stuff + // do more stuff⏪ + var i = 0 + } + } + """ + + let expected = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + // do more stuff + var i = 0 + } + } + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + func testVarOnly() { + let input = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + // do more stuff + ⏩⏪var i = 0 + } + } + """ + + let expected = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + // do more stuff + var i = 0 + } + } + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + func testSingleLineFunc() { + let input = + """ + func foo() ⏩{}⏪ + """ + + let expected = + """ + func foo() {} + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + func testSingleLineFunc2() { + let input = + """ + func foo() /**/ ⏩{}⏪ + """ + + let expected = + """ + func foo() /**/ {} + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + func testSimpleFunc() { + let input = + """ + func foo() /**/ + ⏩{}⏪ + """ + + let expected = + """ + func foo() /**/ + {} + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + // MARK: - multiple selection ranges + func testFirstCommentAndVar() { + let input = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + ⏩⏪// do stuff + // do more stuff + ⏩⏪var i = 0 + } + } + """ + + let expected = + """ + func foo() { + if let SomeReallyLongVar = Some.More.Stuff(), let a = myfunc() { + // do stuff + // do more stuff + var i = 0 + } + } + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } + + // from AccessorTests (but with some Selection ranges) + func testBasicAccessors() { + let input = + """ + ⏩struct MyStruct { + var memberValue: Int + var someValue: Int { get { return memberValue + 2 } set(newValue) { memberValue = newValue } } + }⏪ + struct MyStruct { + var memberValue: Int + var someValue: Int { @objc get { return memberValue + 2 } @objc(isEnabled) set(newValue) { memberValue = newValue } } + } + struct MyStruct { + var memberValue: Int + var memberValue2: Int + var someValue: Int { + get { + let A = 123 + return A + } + set(newValue) { + memberValue = newValue && otherValue + ⏩memberValue2 = newValue / 2 && andableValue⏪ + } + } + } + struct MyStruct { + var memberValue: Int + var SomeValue: Int { return 123 } + var AnotherValue: Double { + let out = 1.23 + return out + } + } + """ + + let expected = + """ + struct MyStruct { + var memberValue: Int + var someValue: Int { + get { return memberValue + 2 } + set(newValue) { memberValue = newValue } + } + } + struct MyStruct { + var memberValue: Int + var someValue: Int { @objc get { return memberValue + 2 } @objc(isEnabled) set(newValue) { memberValue = newValue } } + } + struct MyStruct { + var memberValue: Int + var memberValue2: Int + var someValue: Int { + get { + let A = 123 + return A + } + set(newValue) { + memberValue = newValue && otherValue + memberValue2 = + newValue / 2 && andableValue + } + } + } + struct MyStruct { + var memberValue: Int + var SomeValue: Int { return 123 } + var AnotherValue: Double { + let out = 1.23 + return out + } + } + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 45) + } + + // from CommentTests (but with some Selection ranges) + func testContainerLineComments() { + let input = + """ + // Array comment + let a = [⏩4⏪56, // small comment + 789] + + // Dictionary comment + let b = ["abc": ⏩456, // small comment + "def": 789]⏪ + + // Trailing comment + let c = [123, 456 // small comment + ] + + ⏩/* Array comment */ + let a = [456, /* small comment */ + 789] + + /* Dictionary comment */ + let b = ["abc": 456, /* small comment */ + "def": 789]⏪ + """ + + let expected = + """ + // Array comment + let a = [ + 456, // small comment + 789] + + // Dictionary comment + let b = ["abc": 456, // small comment + "def": 789, + ] + + // Trailing comment + let c = [123, 456 // small comment + ] + + /* Array comment */ + let a = [ + 456, /* small comment */ + 789, + ] + + /* Dictionary comment */ + let b = [ + "abc": 456, /* small comment */ + "def": 789, + ] + """ + + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80) + } +} diff --git a/Tests/SwiftFormatTests/PrettyPrint/WhitespaceTestCase.swift b/Tests/SwiftFormatTests/PrettyPrint/WhitespaceTestCase.swift index 1add23434..78c49752e 100644 --- a/Tests/SwiftFormatTests/PrettyPrint/WhitespaceTestCase.swift +++ b/Tests/SwiftFormatTests/PrettyPrint/WhitespaceTestCase.swift @@ -39,6 +39,7 @@ class WhitespaceTestCase: DiagnosingTestCase { let context = makeContext( sourceFileSyntax: sourceFileSyntax, configuration: configuration, + selection: .infinite, findingConsumer: { emittedFindings.append($0) }) let linter = WhitespaceLinter( user: markedText.textWithoutMarkers, formatted: expected, context: context) diff --git a/Tests/SwiftFormatTests/Rules/BeginDocumentationCommentWithOneLineSummaryTests.swift b/Tests/SwiftFormatTests/Rules/BeginDocumentationCommentWithOneLineSummaryTests.swift index e41425930..cfeaa09dd 100644 --- a/Tests/SwiftFormatTests/Rules/BeginDocumentationCommentWithOneLineSummaryTests.swift +++ b/Tests/SwiftFormatTests/Rules/BeginDocumentationCommentWithOneLineSummaryTests.swift @@ -14,33 +14,33 @@ final class BeginDocumentationCommentWithOneLineSummaryTests: LintOrFormatRuleTe assertLint( BeginDocumentationCommentWithOneLineSummary.self, """ - /// Returns a bottle of Dr Pepper from the vending machine. - public func drPepper(from vendingMachine: VendingMachine) -> Soda {} + /// Returns a bottle of Dr Pepper from the vending machine. + public func drPepper(from vendingMachine: VendingMachine) -> Soda {} - /// Contains a comment as description that needs a sentence - /// of two lines of code. - public var twoLinesForOneSentence = "test" + /// Contains a comment as description that needs a sentence + /// of two lines of code. + public var twoLinesForOneSentence = "test" - /// The background color of the view. - var backgroundColor: UIColor + /// The background color of the view. + var backgroundColor: UIColor - /// Returns the sum of the numbers. - /// - /// - Parameter numbers: The numbers to sum. - /// - Returns: The sum of the numbers. - func sum(_ numbers: [Int]) -> Int { - // ... - } + /// Returns the sum of the numbers. + /// + /// - Parameter numbers: The numbers to sum. + /// - Returns: The sum of the numbers. + func sum(_ numbers: [Int]) -> Int { + // ... + } - /// This docline should not succeed. - /// There are two sentences without a blank line between them. - 1️⃣struct Test {} + /// This docline should not succeed. + /// There are two sentences without a blank line between them. + 1️⃣struct Test {} - /// This docline should not succeed. There are two sentences. - 2️⃣public enum Token { case comma, semicolon, identifier } + /// This docline should not succeed. There are two sentences. + 2️⃣public enum Token { case comma, semicolon, identifier } - /// Should fail because it doesn't have a period - 3️⃣public class testNoPeriod {} + /// Should fail because it doesn't have a period + 3️⃣public class testNoPeriod {} """, findings: [ FindingSpec("1️⃣", message: #"add a blank comment line after this sentence: "This docline should not succeed.""#), @@ -54,36 +54,36 @@ final class BeginDocumentationCommentWithOneLineSummaryTests: LintOrFormatRuleTe assertLint( BeginDocumentationCommentWithOneLineSummary.self, """ - /** - * Returns the numeric value. - * - * - Parameters: - * - digit: The Unicode scalar whose numeric value should be returned. - * - radix: The radix, between 2 and 36, used to compute the numeric value. - * - Returns: The numeric value of the scalar.*/ - func numericValue(of digit: UnicodeScalar, radix: Int = 10) -> Int {} - - /** - * This block comment contains a sentence summary - * of two lines of code. - */ - public var twoLinesForOneSentence = "test" - - /** - * This block comment should not succeed, struct. - * There are two sentences without a blank line between them. - */ - 1️⃣struct TestStruct {} - - /** - This block comment should not succeed, class. - Add a blank comment after the first line. - */ - 2️⃣public class TestClass {} - /** This block comment should not succeed, enum. There are two sentences. */ - 3️⃣public enum testEnum {} - /** Should fail because it doesn't have a period */ - 4️⃣public class testNoPeriod {} + /** + * Returns the numeric value. + * + * - Parameters: + * - digit: The Unicode scalar whose numeric value should be returned. + * - radix: The radix, between 2 and 36, used to compute the numeric value. + * - Returns: The numeric value of the scalar.*/ + func numericValue(of digit: UnicodeScalar, radix: Int = 10) -> Int {} + + /** + * This block comment contains a sentence summary + * of two lines of code. + */ + public var twoLinesForOneSentence = "test" + + /** + * This block comment should not succeed, struct. + * There are two sentences without a blank line between them. + */ + 1️⃣struct TestStruct {} + + /** + This block comment should not succeed, class. + Add a blank comment after the first line. + */ + 2️⃣public class TestClass {} + /** This block comment should not succeed, enum. There are two sentences. */ + 3️⃣public enum testEnum {} + /** Should fail because it doesn't have a period */ + 4️⃣public class testNoPeriod {} """, findings: [ FindingSpec("1️⃣", message: #"add a blank comment line after this sentence: "This block comment should not succeed, struct.""#), diff --git a/Tests/SwiftFormatTests/Rules/LintOrFormatRuleTestCase.swift b/Tests/SwiftFormatTests/Rules/LintOrFormatRuleTestCase.swift index e989bd804..814952e6d 100644 --- a/Tests/SwiftFormatTests/Rules/LintOrFormatRuleTestCase.swift +++ b/Tests/SwiftFormatTests/Rules/LintOrFormatRuleTestCase.swift @@ -27,7 +27,8 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase { line: UInt = #line ) { let markedText = MarkedText(textWithMarkers: markedSource) - let tree = Parser.parse(source: markedText.textWithoutMarkers) + let unmarkedSource = markedText.textWithoutMarkers + let tree = Parser.parse(source: unmarkedSource) let sourceFileSyntax = try! OperatorTable.standardOperators.foldAll(tree).as(SourceFileSyntax.self)! @@ -39,6 +40,7 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase { let context = makeContext( sourceFileSyntax: sourceFileSyntax, configuration: configuration, + selection: .infinite, findingConsumer: { emittedFindings.append($0) }) let linter = type.init(context: context) linter.walk(sourceFileSyntax) @@ -60,6 +62,7 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase { pipeline.debugOptions.insert(.disablePrettyPrint) try! pipeline.lint( syntax: sourceFileSyntax, + source: unmarkedSource, operatorTable: OperatorTable.standardOperators, assumingFileURL: URL(string: file.description)!) @@ -96,7 +99,8 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase { line: UInt = #line ) { let markedInput = MarkedText(textWithMarkers: input) - let tree = Parser.parse(source: markedInput.textWithoutMarkers) + let originalSource: String = markedInput.textWithoutMarkers + let tree = Parser.parse(source: originalSource) let sourceFileSyntax = try! OperatorTable.standardOperators.foldAll(tree).as(SourceFileSyntax.self)! @@ -108,6 +112,7 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase { let context = makeContext( sourceFileSyntax: sourceFileSyntax, configuration: configuration, + selection: .infinite, findingConsumer: { emittedFindings.append($0) }) let formatter = formatType.init(context: context) @@ -129,6 +134,7 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase { // misplacing trivia in a way that the pretty-printer isn't able to handle). let prettyPrintedSource = PrettyPrinter( context: context, + source: originalSource, node: Syntax(actual), printTokenStream: false, whitespaceOnly: false @@ -148,8 +154,8 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase { pipeline.debugOptions.insert(.disablePrettyPrint) var pipelineActual = "" try! pipeline.format( - syntax: sourceFileSyntax, operatorTable: OperatorTable.standardOperators, - assumingFileURL: nil, to: &pipelineActual) + syntax: sourceFileSyntax, source: originalSource, operatorTable: OperatorTable.standardOperators, + assumingFileURL: nil, selection: .infinite, to: &pipelineActual) assertStringsEqualWithDiff(pipelineActual, expected) assertFindings( expected: findings, markerLocations: markedInput.markers, diff --git a/cmake/modules/SwiftSupport.cmake b/cmake/modules/SwiftSupport.cmake index 35decfcca..c37055a33 100644 --- a/cmake/modules/SwiftSupport.cmake +++ b/cmake/modules/SwiftSupport.cmake @@ -47,6 +47,8 @@ function(get_swift_host_arch result_var_name) set("${result_var_name}" "i686" PARENT_SCOPE) elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "wasm32") set("${result_var_name}" "wasm32" PARENT_SCOPE) + elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "riscv64") + set("${result_var_name}" "riscv64" PARENT_SCOPE) else() message(FATAL_ERROR "Unrecognized architecture on host system: ${CMAKE_SYSTEM_PROCESSOR}") endif()