Skip to content

Commit f22aade

Browse files
authored
Merge pull request #194 from allevato/5.2-cherrypicks
Cherry-pick recent 5.2-compatible changes into the 5.2 branch.
2 parents 1fda398 + e829789 commit f22aade

34 files changed

+1720
-386
lines changed

Sources/SwiftFormatPrettyPrint/PrettyPrint.swift

+11-11
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ public class PrettyPrinter {
211211
numberToPrint = consecutiveNewlineCount == 0 ? 1 : 0
212212
case .soft(let count, _):
213213
// We add 1 to the max blank lines because it takes 2 newlines to create the first blank line.
214-
numberToPrint = min(count - consecutiveNewlineCount, configuration.maximumBlankLines + 1)
214+
numberToPrint = min(count, configuration.maximumBlankLines + 1) - consecutiveNewlineCount
215215
case .hard(let count):
216216
numberToPrint = count
217217
}
@@ -359,11 +359,14 @@ public class PrettyPrinter {
359359
= openCloseBreakCompensatingLineNumber != matchingOpenBreak.lineNumber
360360

361361
if matchingOpenBreak.contributesBlockIndent {
362+
// The actual line number is used, instead of the compensating line number. When the close
363+
// break is at the start of a new line, the block indentation isn't carried to the new line.
364+
let currentLine = lineNumber
362365
// When two or more open breaks are encountered on the same line, only the final open
363366
// break is allowed to increase the block indent, avoiding multiple block indents. As the
364367
// open breaks on that line are closed, the new final open break must be enabled again to
365368
// add a block indent.
366-
if matchingOpenBreak.lineNumber == openCloseBreakCompensatingLineNumber,
369+
if matchingOpenBreak.lineNumber == currentLine,
367370
let lastActiveOpenBreak = activeOpenBreaks.last,
368371
lastActiveOpenBreak.kind == .block,
369372
!lastActiveOpenBreak.contributesBlockIndent
@@ -651,15 +654,12 @@ public class PrettyPrinter {
651654
lengths.append(0)
652655

653656
case .commaDelimitedRegionEnd:
654-
// The trailing comma needs to be included in the length of the preceding break, but is not
655-
// included in the length of the enclosing group. A trailing comma cannot cause the group
656-
// to break onto multiple lines, because the comma isn't printed for a single line group.
657-
if let index = delimIndexStack.last, case .break = tokens[index] {
658-
lengths[index] += 1
659-
}
660-
// If the closest delimiter token is an open, instead of a break, then adding the comma's
661-
// length isn't necessary. In that case, the comma is printed if the preceding break fires.
662-
657+
// The token's length is only necessary when a comma will be printed, but it's impossible to
658+
// know at this point whether the region-start token will be on the same line as this token.
659+
// Without adding this length to the total, it would be possible for this comma to be
660+
// printed in column `maxLineLength`. Unfortunately, this can cause breaks to fire
661+
// unnecessarily when the enclosed tokens comma would fit within `maxLineLength`.
662+
total += 1
663663
lengths.append(1)
664664
}
665665
}

Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift

+152-48
Large diffs are not rendered by default.

Sources/SwiftFormatRules/NoEmptyTrailingClosureParentheses.swift

+15-6
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,31 @@ import SwiftSyntax
2222
public final class NoEmptyTrailingClosureParentheses: SyntaxFormatRule {
2323

2424
public override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax {
25-
guard node.argumentList.count == 0 else { return ExprSyntax(node) }
25+
guard node.argumentList.count == 0 else { return super.visit(node) }
2626

27-
guard node.trailingClosure != nil && node.argumentList.isEmpty && node.leftParen != nil else {
28-
return ExprSyntax(node)
27+
guard let trailingClosure = node.trailingClosure,
28+
node.argumentList.isEmpty && node.leftParen != nil else
29+
{
30+
return super.visit(node)
2931
}
3032
guard let name = node.calledExpression.lastToken?.withoutTrivia() else {
31-
return ExprSyntax(node)
33+
return super.visit(node)
3234
}
3335

3436
diagnose(.removeEmptyTrailingParentheses(name: "\(name)"), on: node)
3537

38+
// Need to visit `calledExpression` before creating a new node so that the location data (column
39+
// and line numbers) is available.
40+
guard let rewrittenCalledExpr = ExprSyntax(visit(Syntax(node.calledExpression))) else {
41+
return super.visit(node)
42+
}
3643
let formattedExp = replaceTrivia(
37-
on: node.calledExpression,
38-
token: node.calledExpression.lastToken,
44+
on: rewrittenCalledExpr,
45+
token: rewrittenCalledExpr.lastToken,
3946
trailingTrivia: .spaces(1))
47+
let formattedClosure = visit(trailingClosure).as(ClosureExprSyntax.self)
4048
let result = node.withLeftParen(nil).withRightParen(nil).withCalledExpression(formattedExp)
49+
.withTrailingClosure(formattedClosure)
4150
return ExprSyntax(result)
4251
}
4352
}

Sources/SwiftFormatRules/ReturnVoidInsteadOfEmptyTuple.swift

+73-5
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,82 @@ public final class ReturnVoidInsteadOfEmptyTuple: SyntaxFormatRule {
2525
public override func visit(_ node: FunctionTypeSyntax) -> TypeSyntax {
2626
guard let returnType = node.returnType.as(TupleTypeSyntax.self),
2727
returnType.elements.count == 0
28-
else { return TypeSyntax(node) }
29-
diagnose(.returnVoid, on: node.returnType)
30-
let voidKeyword = SyntaxFactory.makeSimpleTypeIdentifier(
28+
else {
29+
return super.visit(node)
30+
}
31+
32+
diagnose(.returnVoid, on: returnType)
33+
34+
// If the user has put non-whitespace trivia inside the empty tuple, like a comment, then we
35+
// still diagnose it as a lint error but we don't replace it because it's not obvious where the
36+
// comment should go.
37+
if hasNonWhitespaceLeadingTrivia(returnType.rightParen) {
38+
return super.visit(node)
39+
}
40+
41+
// Make sure that function types nested in the argument list are also rewritten (for example,
42+
// `(Int -> ()) -> ()` should become `(Int -> Void) -> Void`).
43+
let arguments = visit(node.arguments).as(TupleTypeElementListSyntax.self)!
44+
let voidKeyword = makeVoidIdentifierType(toReplace: returnType)
45+
return TypeSyntax(node.withArguments(arguments).withReturnType(TypeSyntax(voidKeyword)))
46+
}
47+
48+
public override func visit(_ node: ClosureSignatureSyntax) -> Syntax {
49+
guard let output = node.output,
50+
let returnType = output.returnType.as(TupleTypeSyntax.self),
51+
returnType.elements.count == 0
52+
else {
53+
return super.visit(node)
54+
}
55+
56+
diagnose(.returnVoid, on: returnType)
57+
58+
// If the user has put non-whitespace trivia inside the empty tuple, like a comment, then we
59+
// still diagnose it as a lint error but we don't replace it because it's not obvious where the
60+
// comment should go.
61+
if hasNonWhitespaceLeadingTrivia(returnType.rightParen) {
62+
return super.visit(node)
63+
}
64+
65+
let input: Syntax?
66+
if let parameterClause = node.input?.as(ParameterClauseSyntax.self) {
67+
// If the closure input is a complete parameter clause (variables and types), make sure that
68+
// nested function types are also rewritten (for example, `label: (Int -> ()) -> ()` should
69+
// become `label: (Int -> Void) -> Void`).
70+
input = visit(parameterClause)
71+
} else {
72+
// Otherwise, it's a simple signature (just variable names, no types), so there is nothing to
73+
// rewrite.
74+
input = node.input
75+
}
76+
let voidKeyword = makeVoidIdentifierType(toReplace: returnType)
77+
return Syntax(node.withInput(input).withOutput(output.withReturnType(TypeSyntax(voidKeyword))))
78+
}
79+
80+
/// Returns a value indicating whether the leading trivia of the given token contained any
81+
/// non-whitespace pieces.
82+
private func hasNonWhitespaceLeadingTrivia(_ token: TokenSyntax) -> Bool {
83+
for piece in token.leadingTrivia {
84+
switch piece {
85+
case .blockComment, .docBlockComment, .docLineComment, .garbageText, .lineComment:
86+
return true
87+
default:
88+
break
89+
}
90+
}
91+
return false
92+
}
93+
94+
/// Returns a type syntax node with the identifier `Void` whose leading and trailing trivia have
95+
/// been copied from the tuple type syntax node it is replacing.
96+
private func makeVoidIdentifierType(toReplace node: TupleTypeSyntax) -> SimpleTypeIdentifierSyntax
97+
{
98+
return SyntaxFactory.makeSimpleTypeIdentifier(
3199
name: SyntaxFactory.makeIdentifier(
32100
"Void",
33-
trailingTrivia: returnType.rightParen.trailingTrivia),
101+
leadingTrivia: node.firstToken?.leadingTrivia ?? [],
102+
trailingTrivia: node.lastToken?.trailingTrivia ?? []),
34103
genericArgumentClause: nil)
35-
return TypeSyntax(node.withReturnType(TypeSyntax(voidKeyword)))
36104
}
37105
}
38106

Sources/SwiftFormatRules/UseTripleSlashForDocumentationComments.swift

+8-2
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,23 @@ public final class UseTripleSlashForDocumentationComments: SyntaxFormatRule {
7373
/// a docLineComment.
7474
private func convertDocBlockCommentToDocLineComment(_ decl: DeclSyntax) -> DeclSyntax {
7575
guard let commentText = decl.docComment else { return decl }
76-
guard let declLeadinTrivia = decl.leadingTrivia else { return decl }
76+
guard let declLeadingTrivia = decl.leadingTrivia else { return decl }
7777
let docComments = commentText.components(separatedBy: "\n")
7878
var pieces = [TriviaPiece]()
7979

8080
// Ensures the documentation comment is a docLineComment.
8181
var hasFoundDocComment = false
82-
for piece in declLeadinTrivia.reversed() {
82+
for piece in declLeadingTrivia.reversed() {
8383
if case .docBlockComment(_) = piece, !hasFoundDocComment {
8484
hasFoundDocComment = true
8585
diagnose(.avoidDocBlockComment, on: decl)
8686
pieces.append(contentsOf: separateDocBlockIntoPieces(docComments).reversed())
87+
} else if case .docLineComment(_) = piece, !hasFoundDocComment {
88+
// The comment was a doc-line comment all along. Leave it alone.
89+
// This intentionally only considers the comment closest to the decl. There may be other
90+
// comments, including block or doc-block comments, which are left as-is because they aren't
91+
// necessarily related to the decl and are unlikely part of the decl's documentation.
92+
return decl
8793
} else {
8894
pieces.append(piece)
8995
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
import SwiftFormatConfiguration
15+
16+
/// Loads formatter configurations, caching them in memory so that multiple operations in the same
17+
/// directory do not repeatedly hit the file system.
18+
struct ConfigurationLoader {
19+
/// A mapping from configuration file URLs to the loaded configuration data.
20+
private var cache = [URL: Configuration]()
21+
22+
/// Returns the configuration associated with the configuration file at the given path.
23+
///
24+
/// - Throws: If an error occurred loading the configuration.
25+
mutating func configuration(atPath path: String) throws -> Configuration {
26+
return try configuration(at: URL(fileURLWithPath: path))
27+
}
28+
29+
/// Returns the configuration found by searching in the directory (and ancestor directories)
30+
/// containing the given `.swift` source file.
31+
///
32+
/// If no configuration file was found during the search, this method returns nil.
33+
///
34+
/// - Throws: If a configuration file was found but an error occurred loading it.
35+
mutating func configuration(forSwiftFileAtPath path: String) throws -> Configuration? {
36+
let swiftFileURL = URL(fileURLWithPath: path)
37+
guard let configurationFileURL = Configuration.url(forConfigurationFileApplyingTo: swiftFileURL)
38+
else {
39+
return nil
40+
}
41+
return try configuration(at: configurationFileURL)
42+
}
43+
44+
/// Returns the configuration associated with the configuration file at the given URL.
45+
///
46+
/// - Throws: If an error occurred loading the configuration.
47+
private mutating func configuration(at url: URL) throws -> Configuration {
48+
if let cachedConfiguration = cache[url] {
49+
return cachedConfiguration
50+
}
51+
52+
let configuration = try Configuration(contentsOf: url)
53+
cache[url] = configuration
54+
return configuration
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
import SwiftFormat
15+
import SwiftFormatConfiguration
16+
import SwiftSyntax
17+
18+
/// The frontend for formatting operations.
19+
class FormatFrontend: Frontend {
20+
/// Whether or not to format the Swift file in-place.
21+
private let inPlace: Bool
22+
23+
init(lintFormatOptions: LintFormatOptions, inPlace: Bool) {
24+
self.inPlace = inPlace
25+
super.init(lintFormatOptions: lintFormatOptions)
26+
}
27+
28+
override func processFile(_ fileToProcess: FileToProcess) {
29+
// Even though `diagnosticEngine` is defined, it's use is reserved for fatal messages. Pass nil
30+
// to the formatter to suppress other messages since they will be fixed or can't be
31+
// automatically fixed anyway.
32+
let formatter = SwiftFormatter(
33+
configuration: fileToProcess.configuration, diagnosticEngine: nil)
34+
formatter.debugOptions = debugOptions
35+
36+
let path = fileToProcess.path
37+
guard let source = fileToProcess.sourceText else {
38+
diagnosticEngine.diagnose(
39+
Diagnostic.Message(.error, "Unable to read source for formatting from \(path)."))
40+
return
41+
}
42+
43+
var stdoutStream = FileHandle.standardOutput
44+
do {
45+
let assumingFileURL = URL(fileURLWithPath: path)
46+
if inPlace {
47+
var buffer = ""
48+
try formatter.format(source: source, assumingFileURL: assumingFileURL, to: &buffer)
49+
50+
let bufferData = buffer.data(using: .utf8)! // Conversion to UTF-8 cannot fail
51+
try bufferData.write(to: assumingFileURL, options: .atomic)
52+
} else {
53+
try formatter.format(source: source, assumingFileURL: assumingFileURL, to: &stdoutStream)
54+
}
55+
} catch SwiftFormatError.fileNotReadable {
56+
diagnosticEngine.diagnose(
57+
Diagnostic.Message(
58+
.error, "Unable to format \(path): file is not readable or does not exist."))
59+
return
60+
} catch SwiftFormatError.fileContainsInvalidSyntax(let position) {
61+
guard !lintFormatOptions.ignoreUnparsableFiles else {
62+
guard !inPlace else {
63+
// For in-place mode, nothing is expected to stdout and the file shouldn't be modified.
64+
return
65+
}
66+
stdoutStream.write(source)
67+
return
68+
}
69+
let location = SourceLocationConverter(file: path, source: source).location(for: position)
70+
diagnosticEngine.diagnose(
71+
Diagnostic.Message(.error, "file contains invalid or unrecognized Swift syntax."),
72+
location: location)
73+
return
74+
} catch {
75+
diagnosticEngine.diagnose(Diagnostic.Message(.error, "Unable to format \(path): \(error)"))
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)