Skip to content

Commit d0c5816

Browse files
committed
[OrderedImports] Fix dropped trailing comments on top-level code items.
Also add some validation logic in format rule tests. We already run the pretty printer afterwards to verify that there aren't any invariants broken that would cause an assertion failure, but we don't compare the actual pretty-printed text to the originally transformed tree (because we don't want the output of those tests to be sensitive to pretty-printer configuration). What we *can* do, which is still an improvement, is walk the token sequence and compare the tokens and trivia in a whitespace-insensitive manner. This makes sure that we don't move trivia around in a way that the format rule would accept but that the pretty-printer wouldn't know how to handle.
1 parent a8f5553 commit d0c5816

File tree

3 files changed

+87
-15
lines changed

3 files changed

+87
-15
lines changed

Sources/SwiftFormat/Rules/OrderedImports.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -315,17 +315,17 @@ fileprivate func generateLines(codeBlockItemList: CodeBlockItemListSyntax, conte
315315
blockWithoutTrailingTrivia.trailingTrivia = []
316316
currentLine.syntaxNode = .importCodeBlock(blockWithoutTrailingTrivia, sortable: sortable)
317317
} else {
318-
guard let syntaxNode = currentLine.syntaxNode else {
319-
currentLine.syntaxNode = .nonImportCodeBlocks([block])
320-
continue
321-
}
322-
// Multiple code blocks can be merged, as long as there isn't an import statement.
323-
switch syntaxNode {
324-
case .importCodeBlock:
325-
appendNewLine()
318+
if let syntaxNode = currentLine.syntaxNode {
319+
// Multiple code blocks can be merged, as long as there isn't an import statement.
320+
switch syntaxNode {
321+
case .importCodeBlock:
322+
appendNewLine()
323+
currentLine.syntaxNode = .nonImportCodeBlocks([block])
324+
case .nonImportCodeBlocks(let existingCodeBlocks):
325+
currentLine.syntaxNode = .nonImportCodeBlocks(existingCodeBlocks + [block])
326+
}
327+
} else {
326328
currentLine.syntaxNode = .nonImportCodeBlocks([block])
327-
case .nonImportCodeBlocks(let existingCodeBlocks):
328-
currentLine.syntaxNode = .nonImportCodeBlocks(existingCodeBlocks + [block])
329329
}
330330
}
331331

Tests/SwiftFormatTests/Rules/LintOrFormatRuleTestCase.swift

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase {
112112

113113
let formatter = formatType.init(context: context)
114114
let actual = formatter.visit(sourceFileSyntax)
115-
assertStringsEqualWithDiff(actual.description, expected, file: file, line: line)
115+
assertStringsEqualWithDiff("\(actual)", expected, file: file, line: line)
116116

117117
assertFindings(
118118
expected: findings,
@@ -123,15 +123,22 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase {
123123
line: line)
124124

125125
// Verify that the pretty printer can consume the transformed tree (e.g., it does not contain
126-
// any unfolded `SequenceExpr`s). We don't need to check the actual output here (we don't want
127-
// the rule tests to be pretty-printer dependent), but this will catch invariants that aren't
128-
// satisfied.
129-
_ = PrettyPrinter(
126+
// any unfolded `SequenceExpr`s). Then do a whitespace-insensitive comparison of the two trees
127+
// to verify that the format rule didn't transform the tree in such a way that it caused the
128+
// pretty-printer to drop important information (the most likely case is a format rule
129+
// misplacing trivia in a way that the pretty-printer isn't able to handle).
130+
let prettyPrintedSource = PrettyPrinter(
130131
context: context,
131132
node: Syntax(actual),
132133
printTokenStream: false,
133134
whitespaceOnly: false
134135
).prettyPrint()
136+
let prettyPrintedTree = Parser.parse(source: prettyPrintedSource)
137+
XCTAssertEqual(
138+
whitespaceInsensitiveText(of: actual),
139+
whitespaceInsensitiveText(of: prettyPrintedTree),
140+
"After pretty-printing and removing fluid whitespace, the files did not match",
141+
file: file, line: line)
135142

136143
var emittedPipelineFindings = [Finding]()
137144
// Disable default rules, so only select rule runs in pipeline
@@ -149,3 +156,34 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase {
149156
emittedFindings: emittedPipelineFindings, context: context, file: file, line: line)
150157
}
151158
}
159+
160+
/// Returns a string containing a whitespace-insensitive representation of the given source file.
161+
private func whitespaceInsensitiveText(of file: SourceFileSyntax) -> String {
162+
var result = ""
163+
for token in file.tokens(viewMode: .sourceAccurate) {
164+
appendNonspaceTrivia(token.leadingTrivia, to: &result)
165+
result.append(token.text)
166+
appendNonspaceTrivia(token.trailingTrivia, to: &result)
167+
}
168+
return result
169+
}
170+
171+
/// Appends any non-whitespace trivia pieces from the given trivia collection to the output string.
172+
private func appendNonspaceTrivia(_ trivia: Trivia, to string: inout String) {
173+
for piece in trivia {
174+
switch piece {
175+
case .carriageReturnLineFeeds, .carriageReturns, .formfeeds, .newlines, .spaces, .tabs:
176+
break
177+
case .lineComment(let comment), .docLineComment(let comment):
178+
// A tree transforming rule might leave whitespace at the end of a line comment, which the
179+
// pretty printer will remove, so we should ignore that.
180+
if let lastNonWhitespaceIndex = comment.lastIndex(where: { !$0.isWhitespace }) {
181+
string.append(contentsOf: comment[...lastNonWhitespaceIndex])
182+
} else {
183+
string.append(comment)
184+
}
185+
default:
186+
piece.write(to: &string)
187+
}
188+
}
189+
}

Tests/SwiftFormatTests/Rules/OrderedImportsTests.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,4 +618,38 @@ final class OrderedImportsTests: LintOrFormatRuleTestCase {
618618
]
619619
)
620620
}
621+
622+
func testTrailingCommentsOnTopLevelCodeItems() {
623+
assertFormatting(
624+
OrderedImports.self,
625+
input: """
626+
import Zebras
627+
1️⃣import Apples
628+
#if canImport(Darwin)
629+
import Darwin
630+
#elseif canImport(Glibc)
631+
import Glibc
632+
#endif // canImport(Darwin)
633+
634+
foo() // calls the foo
635+
bar() // calls the bar
636+
""",
637+
expected: """
638+
import Apples
639+
import Zebras
640+
641+
#if canImport(Darwin)
642+
import Darwin
643+
#elseif canImport(Glibc)
644+
import Glibc
645+
#endif // canImport(Darwin)
646+
647+
foo() // calls the foo
648+
bar() // calls the bar
649+
""",
650+
findings: [
651+
FindingSpec("1️⃣", message: "sort import statements lexicographically"),
652+
]
653+
)
654+
}
621655
}

0 commit comments

Comments
 (0)