Skip to content

Commit fe2eebd

Browse files
authored
Merge pull request swiftlang#122 from dylansturg/handle_nonwhitespace_linting
Diagnostics for trailing comma changes.
2 parents cbc37a0 + 3a7779f commit fe2eebd

File tree

8 files changed

+169
-14
lines changed

8 files changed

+169
-14
lines changed

Sources/SwiftFormat/SwiftFormatter.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ public final class SwiftFormatter {
109109
context: context,
110110
operatorContext: operatorContext,
111111
node: transformedSyntax,
112-
printTokenStream: debugOptions.contains(.dumpTokenStream))
112+
printTokenStream: debugOptions.contains(.dumpTokenStream),
113+
whitespaceOnly: false)
113114
outputStream.write(printer.prettyPrint())
114115
}
115116
}

Sources/SwiftFormat/SwiftLinter.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ public final class SwiftLinter {
9292
context: context,
9393
operatorContext: operatorContext,
9494
node: syntax,
95-
printTokenStream: debugOptions.contains(.dumpTokenStream))
95+
printTokenStream: debugOptions.contains(.dumpTokenStream),
96+
whitespaceOnly: true)
9697
let formatted = printer.prettyPrint()
9798
let ws = WhitespaceLinter(user: syntax.description, formatted: formatted, context: context)
9899
ws.lint()

Sources/SwiftFormatPrettyPrint/PrettyPrint.swift

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ public class PrettyPrinter {
6666
/// If true, the token stream is printed to the console for debugging purposes.
6767
private var printTokenStream: Bool
6868

69+
/// Whether the pretty printer should restrict its changes to whitespace. When true, only
70+
/// whitespace (e.g. spaces, newlines) are modified. Otherwise, text changes (e.g. add/remove
71+
/// trailing commas) are performed in addition to whitespace.
72+
private let whitespaceOnly: Bool
73+
6974
/// Keeps track of the line numbers and indentation states of the open (and unclosed) breaks seen
7075
/// so far.
7176
private var activeOpenBreaks: [ActiveOpenBreak] = []
@@ -139,8 +144,10 @@ public class PrettyPrinter {
139144
/// - node: The node to be pretty printed.
140145
/// - printTokenStream: Indicates whether debug information about the token stream should be
141146
/// printed to standard output.
147+
/// - whitespaceOnly: Whether only whitespace changes should be made.
142148
public init(
143-
context: Context, operatorContext: OperatorContext, node: Syntax, printTokenStream: Bool
149+
context: Context, operatorContext: OperatorContext, node: Syntax, printTokenStream: Bool,
150+
whitespaceOnly: Bool
144151
) {
145152
self.context = context
146153
let configuration = context.configuration
@@ -149,6 +156,7 @@ public class PrettyPrinter {
149156
self.maxLineLength = configuration.lineLength
150157
self.spaceRemaining = self.maxLineLength
151158
self.printTokenStream = printTokenStream
159+
self.whitespaceOnly = whitespaceOnly
152160
}
153161

154162
/// Append the given string to the output buffer.
@@ -451,12 +459,20 @@ public class PrettyPrinter {
451459
case .commaDelimitedRegionStart:
452460
commaDelimitedRegionStack.append(openCloseBreakCompensatingLineNumber)
453461

454-
case .commaDelimitedRegionEnd:
462+
case .commaDelimitedRegionEnd(let hasTrailingComma, let position):
455463
guard let startLineNumber = commaDelimitedRegionStack.popLast() else {
456464
fatalError("Found trailing comma end with no corresponding start.")
457465
}
458466

459-
if startLineNumber != openCloseBreakCompensatingLineNumber {
467+
let shouldHaveTrailingComma = startLineNumber != openCloseBreakCompensatingLineNumber
468+
if shouldHaveTrailingComma && !hasTrailingComma {
469+
diagnose(.addTrailingComma, at: position)
470+
} else if !shouldHaveTrailingComma && hasTrailingComma {
471+
diagnose(.removeTrailingComma, at: position)
472+
}
473+
474+
let shouldWriteComma = whitespaceOnly ? hasTrailingComma : shouldHaveTrailingComma
475+
if shouldWriteComma {
460476
write(",")
461477
}
462478
}
@@ -679,4 +695,10 @@ extension Diagnostic.Message {
679695

680696
static let moveEndOfLineComment = Diagnostic.Message(
681697
.warning, "move end-of-line comment that exceeds the line length")
698+
699+
static let addTrailingComma = Diagnostic.Message(
700+
.warning, "add trailing comma to the last element in multiline collection literal")
701+
702+
static let removeTrailingComma = Diagnostic.Message(
703+
.warning, "remove trailing comma from the last element in single line collection literal")
682704
}

Sources/SwiftFormatPrettyPrint/Token.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ enum Token {
177177

178178
/// Marks the end of a comma delimited collection, where a trailing comma should be inserted
179179
/// if and only if the collection spans multiple lines.
180-
case commaDelimitedRegionEnd
180+
case commaDelimitedRegionEnd(hasTrailingComma: Bool, position: AbsolutePosition)
181181

182182
// Convenience overloads for the enum types
183183
static let open = Token.open(.inconsistent, 0)

Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -709,7 +709,9 @@ private final class TokenStreamCreator: SyntaxVisitor {
709709
insertTokens(.break(.same), betweenElementsOf: node)
710710
if let firstElement = node.first, let lastElement = node.last {
711711
before(firstElement.firstToken, tokens: .commaDelimitedRegionStart)
712-
after(lastElement.lastToken, tokens: .commaDelimitedRegionEnd)
712+
let endToken = Token.commaDelimitedRegionEnd(
713+
hasTrailingComma: lastElement.trailingComma != nil, position: lastElement.endPosition)
714+
after(lastElement.lastToken, tokens: endToken)
713715
if let existingTrailingComma = lastElement.trailingComma {
714716
ignoredTokens.insert(existingTrailingComma)
715717
}
@@ -733,7 +735,9 @@ private final class TokenStreamCreator: SyntaxVisitor {
733735
insertTokens(.break(.same), betweenElementsOf: node)
734736
if let firstElement = node.first, let lastElement = node.last {
735737
before(firstElement.firstToken, tokens: .commaDelimitedRegionStart)
736-
after(lastElement.lastToken, tokens: .commaDelimitedRegionEnd)
738+
let endToken = Token.commaDelimitedRegionEnd(
739+
hasTrailingComma: lastElement.trailingComma != nil, position: lastElement.endPosition)
740+
after(lastElement.lastToken, tokens: endToken)
737741
if let existingTrailingComma = lastElement.trailingComma {
738742
ignoredTokens.insert(existingTrailingComma)
739743
}

Tests/SwiftFormatPrettyPrintTests/ArrayDeclTests.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import SwiftSyntax
2+
3+
@testable import SwiftFormatPrettyPrint
4+
15
public class ArrayDeclTests: PrettyPrintTestCase {
26
public func testBasicArrays() {
37
let input =
@@ -60,4 +64,35 @@ public class ArrayDeclTests: PrettyPrintTestCase {
6064

6165
assertPrettyPrintEqual(input: input, expected: expected, linelength: 45)
6266
}
67+
68+
public func testWhitespaceOnlyDoesNotChangeTrailingComma() {
69+
let input =
70+
"""
71+
let a = [1, 2, 3,]
72+
let a: [String] = [
73+
"One", "Two", "Three", "Four", "Five",
74+
"Six", "Seven", "Eight"
75+
]
76+
"""
77+
78+
assertPrettyPrintEqual(
79+
input: input, expected: input + "\n", linelength: 45, whitespaceOnly: true)
80+
}
81+
82+
public func testTrailingCommaDiagnostics() {
83+
let input =
84+
"""
85+
let a = [1, 2, 3,]
86+
let a: [String] = [
87+
"One", "Two", "Three", "Four", "Five",
88+
"Six", "Seven", "Eight"
89+
]
90+
"""
91+
92+
assertPrettyPrintEqual(
93+
input: input, expected: input + "\n", linelength: 45, whitespaceOnly: true)
94+
95+
XCTAssertDiagnosed(Diagnostic.Message.removeTrailingComma, line: 1, column: 18)
96+
XCTAssertDiagnosed(Diagnostic.Message.addTrailingComma, line: 4, column: 26)
97+
}
6398
}

Tests/SwiftFormatPrettyPrintTests/DictionaryDeclTests.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import SwiftSyntax
2+
3+
@testable import SwiftFormatPrettyPrint
4+
15
public class DictionaryDeclTests: PrettyPrintTestCase {
26
public func testBasicDictionaries() {
37
let input =
@@ -43,4 +47,35 @@ public class DictionaryDeclTests: PrettyPrintTestCase {
4347

4448
assertPrettyPrintEqual(input: input, expected: expected, linelength: 50)
4549
}
50+
51+
public func testWhitespaceOnlyDoesNotChangeTrailingComma() {
52+
let input =
53+
"""
54+
let a = [1: "a", 2: "b", 3: "c",]
55+
let a: [Int: String] = [
56+
1: "a", 2: "b", 3: "c", 4: "d", 5: "e", 6: "f",
57+
7: "g", 8: "i"
58+
]
59+
"""
60+
61+
assertPrettyPrintEqual(
62+
input: input, expected: input + "\n", linelength: 50, whitespaceOnly: true)
63+
}
64+
65+
public func testTrailingCommaDiagnostics() {
66+
let input =
67+
"""
68+
let a = [1: "a", 2: "b", 3: "c",]
69+
let a: [Int: String] = [
70+
1: "a", 2: "b", 3: "c", 4: "d", 5: "e", 6: "f",
71+
7: "g", 8: "i"
72+
]
73+
"""
74+
75+
assertPrettyPrintEqual(
76+
input: input, expected: input + "\n", linelength: 50, whitespaceOnly: true)
77+
78+
XCTAssertDiagnosed(Diagnostic.Message.removeTrailingComma, line: 1, column: 33)
79+
XCTAssertDiagnosed(Diagnostic.Message.addTrailingComma, line: 4, column: 17)
80+
}
4681
}

Tests/SwiftFormatPrettyPrintTests/PrettyPrintTestCase.swift

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,37 +6,90 @@ import XCTest
66
@testable import SwiftFormatPrettyPrint
77

88
public class PrettyPrintTestCase: XCTestCase {
9+
/// A helper that will keep track of which diagnostics have been emitted, and their locations.
10+
private var consumer: DiagnosticTrackingConsumer? = nil
11+
12+
/// Implements `DiagnosticConsumer` to keep track which diagnostics have been raised and their
13+
/// locations.
14+
private class DiagnosticTrackingConsumer: DiagnosticConsumer {
15+
var registeredDiagnostics = [(String, line: Int?, column: Int?)]()
16+
17+
func handle(_ diagnostic: Diagnostic) {
18+
let loc = diagnostic.location
19+
registeredDiagnostics.append((diagnostic.message.text, line: loc?.line, column: loc?.column))
20+
}
21+
22+
func finalize() {}
23+
}
24+
25+
/// Asserts that a specific diagnostic message was emitted. This should be called to check for
26+
/// diagnostics after `assertPrettyPrintEqual`.
27+
///
28+
/// - Parameters:
29+
/// - message: The diagnostic message to check for.
30+
/// - line: The line number of the diagnotic message within the ueser's input text.
31+
/// - column: The column number of the diagnostic message within the user's input text.
32+
/// - file: The file the test resides in (defaults to the current caller's file).
33+
/// - sourceLine: The line the test resides in (defaults to the current caller's file).
34+
func XCTAssertDiagnosed(
35+
_ message: Diagnostic.Message,
36+
line: Int? = nil,
37+
column: Int? = nil,
38+
file: StaticString = #file,
39+
sourceLine: UInt = #line
40+
) {
41+
let maybeIdx = consumer?.registeredDiagnostics.firstIndex {
42+
$0 == (message.text, line: line, column: column)
43+
}
44+
45+
guard let idx = maybeIdx else {
46+
XCTFail("diagnostic '\(message.text)' not raised", file: file, line: sourceLine)
47+
return
48+
}
49+
50+
consumer?.registeredDiagnostics.remove(at: idx)
51+
}
952

1053
public func assertPrettyPrintEqual(
1154
input: String,
1255
expected: String,
1356
linelength: Int,
1457
configuration: Configuration = Configuration(),
58+
whitespaceOnly: Bool = false,
1559
file: StaticString = #file,
1660
line: UInt = #line
1761
) {
1862
var configuration = configuration
1963
configuration.lineLength = linelength
64+
let firstPassConsumer = DiagnosticTrackingConsumer()
65+
consumer = firstPassConsumer
2066

2167
// Assert that the input, when formatted, is what we expected.
22-
if let formatted = prettyPrintedSource(input, configuration: configuration) {
68+
if let formatted = prettyPrintedSource(
69+
input, configuration: configuration, whitespaceOnly: whitespaceOnly,
70+
consumer: firstPassConsumer)
71+
{
2372
XCTAssertEqual(
2473
expected, formatted,
2574
"Pretty-printed result was not what was expected",
2675
file: file, line: line)
2776

2877
// Idempotency check: Running the formatter multiple times should not change the outcome.
2978
// Assert that running the formatter again on the previous result keeps it the same.
30-
if let reformatted = prettyPrintedSource(formatted, configuration: configuration) {
79+
if let reformatted = prettyPrintedSource(
80+
formatted, configuration: configuration, whitespaceOnly: whitespaceOnly)
81+
{
3182
XCTAssertEqual(
3283
formatted, reformatted, "Pretty printer is not idempotent", file: file, line: line)
3384
}
3485
}
3586
}
3687

3788
/// Returns the given source code reformatted with the pretty printer.
38-
private func prettyPrintedSource(_ source: String, configuration: Configuration) -> String?
39-
{
89+
private func prettyPrintedSource(
90+
_ source: String, configuration: Configuration, whitespaceOnly: Bool,
91+
consumer: DiagnosticConsumer? = nil
92+
) -> String? {
4093
let sourceFileSyntax: SourceFileSyntax
4194
do {
4295
sourceFileSyntax = try SyntaxParser.parse(source: source)
@@ -47,15 +100,19 @@ public class PrettyPrintTestCase: XCTestCase {
47100

48101
let context = Context(
49102
configuration: configuration,
50-
diagnosticEngine: nil,
103+
diagnosticEngine: DiagnosticEngine(),
51104
fileURL: URL(fileURLWithPath: "/tmp/file.swift"),
52105
sourceFileSyntax: sourceFileSyntax)
106+
if let consumer = consumer {
107+
context.diagnosticEngine?.addConsumer(consumer)
108+
}
53109

54110
let printer = PrettyPrinter(
55111
context: context,
56112
operatorContext: OperatorContext.makeBuiltinOperatorContext(),
57113
node: sourceFileSyntax,
58-
printTokenStream: false)
114+
printTokenStream: false,
115+
whitespaceOnly: whitespaceOnly)
59116
return printer.prettyPrint()
60117
}
61118
}

0 commit comments

Comments
 (0)