Skip to content

Commit 3b43386

Browse files
authored
Merge pull request swiftlang#185 from google/format-idempotent
Stop wrapping end-of-line comments, add idempotency checks.
2 parents 0590f35 + a799fd4 commit 3b43386

File tree

7 files changed

+112
-65
lines changed

7 files changed

+112
-65
lines changed

Sources/SwiftFormatPrettyPrint/Comment.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import Foundation
14+
import SwiftSyntax
1415

1516
struct Comment {
1617
enum Kind {
@@ -38,10 +39,13 @@ struct Comment {
3839

3940
let kind: Kind
4041
var text: [String]
42+
let position: AbsolutePosition?
4143
public var length: Int
4244

43-
init(kind: Kind, text: String) {
45+
init(kind: Kind, text: String, position: AbsolutePosition? = nil) {
4446
self.kind = kind
47+
self.position = position
48+
4549
switch kind {
4650
case .line, .docLine:
4751
self.text = [text]

Sources/SwiftFormatPrettyPrint/PrettyPrint.swift

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ fileprivate let closeGroupMarker = "\u{27ED}"
2929
/// PrettyPrinter takes a Syntax node and outputs a well-formatted, re-indented reproduction of the
3030
/// code as a String.
3131
public class PrettyPrinter {
32-
private let configuration: Configuration
32+
private let context: Context
33+
private var configuration: Configuration { return context.configuration }
3334
private let maxLineLength: Int
3435
private var tokens: [Token]
3536
private var outputBuffer: String = ""
@@ -85,10 +86,11 @@ public class PrettyPrinter {
8586
/// Creates a new PrettyPrinter with the provided formatting configuration.
8687
///
8788
/// - Parameters:
88-
/// - configuration: The configuration used to decide whitespace or breaking behavior.
89+
/// - context: The formatter context.
8990
/// - node: The node to be pretty printed.
90-
public init(configuration: Configuration, node: Syntax, isDebugMode: Bool, printTokenStream: Bool) {
91-
self.configuration = configuration
91+
public init(context: Context, node: Syntax, isDebugMode: Bool, printTokenStream: Bool) {
92+
self.context = context
93+
let configuration = context.configuration
9294
self.tokens = node.makeTokenStream(configuration: configuration)
9395
self.maxLineLength = configuration.lineLength
9496
self.isDebugMode = isDebugMode
@@ -234,7 +236,7 @@ public class PrettyPrinter {
234236
}
235237
spaceRemaining -= syntaxToken.text.count
236238

237-
case .comment(let comment):
239+
case .comment(let comment, let wasEndOfLine):
238240
if lastBreakConsecutive {
239241
// If the last token created a new line, we need to apply indentation.
240242
writeSpaces(lastBreakValue)
@@ -245,7 +247,13 @@ public class PrettyPrinter {
245247
lastBreakValue = 0
246248
}
247249
write(comment.print(indent: lastBreakValue))
248-
spaceRemaining -= comment.length
250+
if wasEndOfLine {
251+
if comment.length > spaceRemaining {
252+
diagnose(.moveEndOfLineComment, at: comment.position)
253+
}
254+
} else {
255+
spaceRemaining -= comment.length
256+
}
249257

250258
case .verbatim(let verbatim):
251259
write(verbatim.print(indent: lastBreakValue))
@@ -351,9 +359,9 @@ public class PrettyPrinter {
351359
total += syntaxToken.text.count
352360
}
353361

354-
case .comment(let comment):
362+
case .comment(let comment, let wasEndOfLine):
355363
lengths.append(comment.length)
356-
total += comment.length
364+
total += wasEndOfLine ? 0 : comment.length
357365

358366
case .verbatim(let verbatim):
359367
var length: Int
@@ -476,17 +484,17 @@ public class PrettyPrinter {
476484
printDebugIndent()
477485
print("[RESET]")
478486

479-
case .comment(let comment):
487+
case .comment(let comment, let wasEndOfLine):
480488
printDebugIndent()
481489
switch comment.kind {
482490
case .line:
483-
print("[COMMENT Line Length: \(length)]")
491+
print("[COMMENT Line Length: \(length) EOL: \(wasEndOfLine)]")
484492
case .docLine:
485-
print("[COMMENT DocLine Length: \(length)]")
493+
print("[COMMENT DocLine Length: \(length) EOL: \(wasEndOfLine)]")
486494
case .block:
487-
print("[COMMENT Block Length: \(length)]")
495+
print("[COMMENT Block Length: \(length) EOL: \(wasEndOfLine)]")
488496
case .docBlock:
489-
print("[COMMENT DocBlock Length: \(length)]")
497+
print("[COMMENT DocBlock Length: \(length) EOL: \(wasEndOfLine)]")
490498
}
491499
printDebugIndent()
492500
print(comment.print(indent: debugIndent))
@@ -514,6 +522,16 @@ public class PrettyPrinter {
514522
}
515523
}
516524
}
525+
526+
private func diagnose(_ message: Diagnostic.Message, at position: AbsolutePosition?) {
527+
let location: SourceLocation?
528+
if let position = position {
529+
location = SourceLocation(file: context.fileURL.path, position: position)
530+
} else {
531+
location = nil
532+
}
533+
context.diagnosticEngine?.diagnose(message, location: location)
534+
}
517535
}
518536

519537
/// Convenience properties/functions to access ANSI color code strings, respecting whether or not
@@ -543,3 +561,9 @@ enum Ansi {
543561
/// The number 30 is added to the given index to determine the actual color code.
544562
static func color(_ index: Int) -> String { return isTerminal ? "\u{001b}[\(30 + index)m" : "" }
545563
}
564+
565+
extension Diagnostic.Message {
566+
567+
static let moveEndOfLineComment =
568+
Diagnostic.Message(.warning, "End-of-line comment exceeds the line length")
569+
}

Sources/SwiftFormatPrettyPrint/Token.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ enum Token {
3131
case `break`(size: Int, offset: Int)
3232
case space(size: Int)
3333
case newlines(Int, offset: Int)
34-
case comment(Comment)
34+
case comment(Comment, wasEndOfLine: Bool)
3535
case reset
3636
case verbatim(Verbatim)
3737

Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,24 +1371,23 @@ private final class TokenStreamCreator: SyntaxVisitor {
13711371
return
13721372
}
13731373

1374+
let commentToken: Comment
1375+
let position = token.endPositionAfterTrailingTrivia
13741376
switch firstPiece {
13751377
case .lineComment(let text):
1376-
appendToken(.break(size: 2, offset: 2))
1377-
appendToken(.comment(Comment(kind: .line, text: text)))
1378-
if isInContainer(token) {
1379-
appendToken(.newline)
1380-
}
1381-
1378+
commentToken = Comment(kind: .line, text: text, position: position)
13821379
case .blockComment(let text):
1383-
appendToken(.break(size: 2, offset: 2))
1384-
appendToken(.comment(Comment(kind: .block, text: text)))
1385-
if isInContainer(token) {
1386-
appendToken(.newline)
1387-
}
1388-
1380+
commentToken = Comment(kind: .block, text: text, position: position)
13891381
default:
13901382
return
13911383
}
1384+
1385+
appendToken(.break(size: 2, offset: 0))
1386+
appendToken(.comment(commentToken, wasEndOfLine: true))
1387+
1388+
if isInContainer(token) {
1389+
appendToken(.newline)
1390+
}
13921391
}
13931392

13941393
private func isInContainer(_ token: TokenSyntax) -> Bool {
@@ -1415,7 +1414,7 @@ private final class TokenStreamCreator: SyntaxVisitor {
14151414
}
14161415
}
14171416

1418-
appendToken(.comment(Comment(kind: .line, text: text)))
1417+
appendToken(.comment(Comment(kind: .line, text: text), wasEndOfLine: false))
14191418

14201419
if token.withoutTrivia().text == "}" {
14211420
appendToken(.newline(offset: -2))
@@ -1435,7 +1434,7 @@ private final class TokenStreamCreator: SyntaxVisitor {
14351434
}
14361435
}
14371436

1438-
appendToken(.comment(Comment(kind: .block, text: text)))
1437+
appendToken(.comment(Comment(kind: .block, text: text), wasEndOfLine: false))
14391438

14401439
if token.withoutTrivia().text == "}" {
14411440
appendToken(.newline(offset: -2))
@@ -1445,7 +1444,7 @@ private final class TokenStreamCreator: SyntaxVisitor {
14451444
}
14461445

14471446
case .docLineComment(let text):
1448-
appendToken(.comment(Comment(kind: .docLine, text: text)))
1447+
appendToken(.comment(Comment(kind: .docLine, text: text), wasEndOfLine: false))
14491448
if case .newlines? = trivia[safe: index + 1],
14501449
case .docLineComment? = trivia[safe: index + 2] {
14511450
// do nothing
@@ -1454,7 +1453,7 @@ private final class TokenStreamCreator: SyntaxVisitor {
14541453
}
14551454

14561455
case .docBlockComment(let text):
1457-
appendToken(.comment(Comment(kind: .docBlock, text: text)))
1456+
appendToken(.comment(Comment(kind: .docBlock, text: text), wasEndOfLine: false))
14581457
appendToken(.newline)
14591458

14601459
case .newlines(let n), .carriageReturns(let n), .carriageReturnLineFeeds(let n):
@@ -1471,11 +1470,11 @@ private final class TokenStreamCreator: SyntaxVisitor {
14711470
func appendToken(_ token: Token) {
14721471
if let last = tokens.last {
14731472
switch (last, token) {
1474-
case (.comment(let c1), .comment(let c2))
1475-
where c1.kind == .docLine && c2.kind == .docLine:
1473+
case (.comment(let c1, _), .comment(let c2, _))
1474+
where c1.kind == .docLine && c2.kind == .docLine:
14761475
var newComment = c1
14771476
newComment.addText(c2.text)
1478-
tokens[tokens.count - 1] = .comment(newComment)
1477+
tokens[tokens.count - 1] = .comment(newComment, wasEndOfLine: false)
14791478
return
14801479

14811480
case (.newlines(let N1, let offset1), .newlines(let N2, let offset2)):

Sources/swift-format/Run.swift

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ import SwiftSyntax
2424
/// - Returns: Zero if there were no lint errors, otherwise a non-zero number.
2525
public func lintMain(path: String) -> Int {
2626
let url = URL(fileURLWithPath: path)
27-
let engine = DiagnosticEngine()
28-
let consumer = PrintingDiagnosticConsumer()
29-
engine.addConsumer(consumer)
27+
let engine = makeDiagnosticEngine()
3028

3129
let context = Context(
3230
configuration: Configuration(),
@@ -55,12 +53,8 @@ public func lintMain(path: String) -> Int {
5553
/// - Returns: Zero if there were no lint errors, otherwise a non-zero number.
5654
public func formatMain(path: String, isDebugMode: Bool, prettyPrint: Bool, printTokenStream: Bool) -> Int {
5755
let url = URL(fileURLWithPath: path)
58-
59-
let context = Context(
60-
configuration: Configuration(),
61-
diagnosticEngine: nil,
62-
fileURL: url
63-
)
56+
let configuration = Configuration()
57+
let context = Context(configuration: configuration, diagnosticEngine: nil, fileURL: url)
6458

6559
let pipeline = FormatPipeline(context: context)
6660
populate(pipeline)
@@ -72,8 +66,15 @@ public func formatMain(path: String, isDebugMode: Bool, prettyPrint: Bool, print
7266
let formatted = pipeline.visit(file as Syntax)
7367

7468
if prettyPrint {
69+
// We create a different context here because we only want diagnostics from the pretty printer
70+
// phase when formatting.
71+
let prettyPrintContext = Context(
72+
configuration: configuration,
73+
diagnosticEngine: makeDiagnosticEngine(),
74+
fileURL: url)
75+
7576
let printer = PrettyPrinter(
76-
configuration: context.configuration,
77+
context: prettyPrintContext,
7778
node: formatted,
7879
isDebugMode: isDebugMode,
7980
printTokenStream: printTokenStream
@@ -87,3 +88,11 @@ public func formatMain(path: String, isDebugMode: Bool, prettyPrint: Bool, print
8788
}
8889
return 0
8990
}
91+
92+
/// Makes and returns a new configured diagnostic engine.
93+
private func makeDiagnosticEngine() -> DiagnosticEngine {
94+
let engine = DiagnosticEngine()
95+
let consumer = PrintingDiagnosticConsumer()
96+
engine.addConsumer(consumer)
97+
return engine
98+
}

Tests/SwiftFormatPrettyPrintTests/CommentTests.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public class CommentTests: PrettyPrintTestCase {
9090
// Comment 3
9191
// Comment 4
9292
93-
let reallyLongVariableName = 123 // This comment should wrap
93+
let reallyLongVariableName = 123 // This comment should not wrap
9494
9595
func MyFun() {
9696
// just a comment
@@ -124,8 +124,7 @@ public class CommentTests: PrettyPrintTestCase {
124124
// Comment 3
125125
// Comment 4
126126
127-
let reallyLongVariableName = 123
128-
// This comment should wrap
127+
let reallyLongVariableName = 123 // This comment should not wrap
129128
130129
func MyFun() {
131130
// just a comment
@@ -298,7 +297,7 @@ public class CommentTests: PrettyPrintTestCase {
298297
/* Comment 3
299298
Comment 4 */
300299
301-
let reallyLongVariableName = 123 /* This comment should wrap */
300+
let reallyLongVariableName = 123 /* This comment should not wrap */
302301
303302
let d = 123
304303
/* Trailing Comment */
@@ -315,8 +314,7 @@ public class CommentTests: PrettyPrintTestCase {
315314
/* Comment 3
316315
Comment 4 */
317316
318-
let reallyLongVariableName = 123
319-
/* This comment should wrap */
317+
let reallyLongVariableName = 123 /* This comment should not wrap */
320318
321319
let d = 123
322320
/* Trailing Comment */

Tests/SwiftFormatPrettyPrintTests/PrettyPrintTestCase.swift

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,48 @@ import XCTest
66
@testable import SwiftFormatPrettyPrint
77

88
public class PrettyPrintTestCase: XCTestCase {
9+
910
public func assertPrettyPrintEqual(
1011
input: String,
1112
expected: String,
1213
linelength: Int,
1314
file: StaticString = #file,
1415
line: UInt = #line
1516
) {
16-
let config = Configuration()
17-
config.lineLength = linelength
17+
let configuration = Configuration()
18+
configuration.lineLength = linelength
1819

1920
let context = Context(
20-
configuration: config,
21+
configuration: configuration,
2122
diagnosticEngine: nil,
22-
fileURL: URL(fileURLWithPath: "/tmp/test.swift")
23-
)
23+
fileURL: URL(fileURLWithPath: "/tmp/file.swift"))
2424

25-
do {
26-
let syntax = try SyntaxTreeParser.parse(input)
25+
// Assert that the input, when formatted, is what we expected.
26+
if let formatted = prettyPrintedSource(input, context: context) {
27+
XCTAssertEqual(
28+
expected, formatted,
29+
"Pretty-printed result was not what was expected",
30+
file: file, line: line)
31+
32+
// Idempotency check: Running the formatter multiple times should not change the outcome.
33+
// Assert that running the formatter again on the previous result keeps it the same.
34+
if let reformatted = prettyPrintedSource(formatted, context: context) {
35+
XCTAssertEqual(
36+
formatted, reformatted, "Pretty printer is not idempotent", file: file, line: line)
37+
}
38+
}
39+
}
2740

41+
/// Returns the given source code reformatted with the pretty printer.
42+
private func prettyPrintedSource(_ original: String, context: Context) -> String? {
43+
do {
44+
let syntax = try SyntaxTreeParser.parse(original)
2845
let printer = PrettyPrinter(
29-
configuration: context.configuration,
30-
node: syntax,
31-
isDebugMode: false,
32-
printTokenStream: false
33-
)
34-
let output = printer.prettyPrint()
35-
XCTAssertEqual(expected, output, file: file, line: line)
46+
context: context, node: syntax, isDebugMode: false, printTokenStream: false)
47+
return printer.prettyPrint()
3648
} catch {
37-
fatalError("\(error)")
49+
XCTFail("Parsing failed with error: \(error)")
50+
return nil
3851
}
3952
}
4053
}

0 commit comments

Comments
 (0)