Skip to content

Commit de7370a

Browse files
committed
Remove the legacy trivia workaround.
This was added when SwiftSyntax changed its behavior regarding trailing trivia -- previously it was only whitespace but now it includes all trivia up to the next newline, including comments (block comments and end-of-line comments). This workaround rewrote trivia to correspond to the old layout before processing the syntax tree further so that we didn't have to overhaul all the rules at the time. However, it isn't good for long-term maintenance for swift-format to be written with assumptions about trivia that aren't consistent with how syntax trees Straight Outta The Parser are formed.
1 parent 2a85d12 commit de7370a

17 files changed

+406
-203
lines changed

Sources/SwiftFormat/Core/LegacyTriviaBehavior.swift

-44
This file was deleted.

Sources/SwiftFormat/Core/Parsing.swift

+1-2
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@ func parseAndEmitDiagnostics(
6363
guard !hasErrors else {
6464
throw SwiftFormatError.fileContainsInvalidSyntax
6565
}
66-
67-
return restoringLegacyTriviaBehavior(sourceFile)
66+
return sourceFile
6867
}
6968

7069
// Wraps a `DiagnosticMessage` but forces its severity to be that of a warning instead of an error.

Sources/SwiftFormat/Core/Rule.swift

+35-12
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@ public protocol Rule {
2929
init(context: Context)
3030
}
3131

32+
/// The part of a node where an emitted finding should be anchored.
33+
@_spi(Rules)
34+
public enum FindingAnchor {
35+
/// The finding is anchored at the beginning of the node's actual content, skipping any leading
36+
/// trivia.
37+
case start
38+
39+
/// The finding is anchored at the beginning of the trivia piece at the given index in the node's
40+
/// leading trivia.
41+
case leadingTrivia(Trivia.Index)
42+
43+
/// The finding is anchored at the beginning of the trivia piece at the given index in the node's
44+
/// trailing trivia.
45+
case trailingTrivia(Trivia.Index)
46+
}
47+
3248
extension Rule {
3349
/// By default, the `ruleName` is just the name of the implementing rule class.
3450
public static var ruleName: String { String("\(self)".split(separator: ".").last!) }
@@ -40,30 +56,37 @@ extension Rule {
4056
/// - node: The syntax node to which the finding should be attached. The finding's location will
4157
/// be set to the start of the node (excluding leading trivia, unless `leadingTriviaIndex` is
4258
/// provided).
43-
/// - leadingTriviaIndex: If non-nil, the index of a trivia piece in the node's leading trivia
44-
/// that should be used to determine the location of the finding. Otherwise, the finding's
45-
/// location will be the start of the node after any leading trivia.
59+
/// - anchor: The part of the node where the finding should be anchored. Defaults to the start
60+
/// of the node's content (after any leading trivia).
4661
/// - notes: An array of notes that provide additional detail about the finding.
4762
public func diagnose<SyntaxType: SyntaxProtocol>(
4863
_ message: Finding.Message,
4964
on node: SyntaxType?,
5065
severity: Finding.Severity? = nil,
51-
leadingTriviaIndex: Trivia.Index? = nil,
66+
anchor: FindingAnchor = .start,
5267
notes: [Finding.Note] = []
5368
) {
5469
let syntaxLocation: SourceLocation?
55-
if let leadingTriviaIndex = leadingTriviaIndex {
56-
syntaxLocation = node?.startLocation(
57-
ofLeadingTriviaAt: leadingTriviaIndex, converter: context.sourceLocationConverter)
70+
if let node = node {
71+
switch anchor {
72+
case .start:
73+
syntaxLocation = node.startLocation(converter: context.sourceLocationConverter)
74+
case .leadingTrivia(let index):
75+
syntaxLocation = node.startLocation(
76+
ofLeadingTriviaAt: index, converter: context.sourceLocationConverter)
77+
case .trailingTrivia(let index):
78+
syntaxLocation = node.startLocation(
79+
ofTrailingTriviaAt: index, converter: context.sourceLocationConverter)
80+
}
5881
} else {
59-
syntaxLocation = node?.startLocation(converter: context.sourceLocationConverter)
82+
syntaxLocation = nil
6083
}
6184

6285
let category = RuleBasedFindingCategory(ruleType: type(of: self), severity: severity)
6386
context.findingEmitter.emit(
64-
message,
65-
category: category,
66-
location: syntaxLocation.flatMap(Finding.Location.init),
67-
notes: notes)
87+
message,
88+
category: category,
89+
location: syntaxLocation.flatMap(Finding.Location.init),
90+
notes: notes)
6891
}
6992
}

Sources/SwiftFormat/Core/SyntaxProtocol+Convenience.swift

+93
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,29 @@ extension SyntaxProtocol {
3636
return self.position + offset
3737
}
3838

39+
/// Returns the absolute position of the trivia piece at the given index in the receiver's
40+
/// trailing trivia collection.
41+
///
42+
/// If the trivia piece spans multiple characters, the value returned is the position of the first
43+
/// character.
44+
///
45+
/// - Precondition: `index` is a valid index in the receiver's trailing trivia collection.
46+
///
47+
/// - Parameter index: The index of the trivia piece in the trailing trivia whose position should
48+
/// be returned.
49+
/// - Returns: The absolute position of the trivia piece.
50+
func position(ofTrailingTriviaAt index: Trivia.Index) -> AbsolutePosition {
51+
guard trailingTrivia.indices.contains(index) else {
52+
preconditionFailure("Index was out of bounds in the node's trailing trivia.")
53+
}
54+
55+
var offset = SourceLength.zero
56+
for currentIndex in trailingTrivia.startIndex..<index {
57+
offset += trailingTrivia[currentIndex].sourceLength
58+
}
59+
return self.endPositionBeforeTrailingTrivia + offset
60+
}
61+
3962
/// Returns the source location of the trivia piece at the given index in the receiver's leading
4063
/// trivia collection.
4164
///
@@ -56,6 +79,76 @@ extension SyntaxProtocol {
5679
) -> SourceLocation {
5780
return converter.location(for: position(ofLeadingTriviaAt: index))
5881
}
82+
83+
/// Returns the source location of the trivia piece at the given index in the receiver's trailing
84+
/// trivia collection.
85+
///
86+
/// If the trivia piece spans multiple characters, the value returned is the location of the first
87+
/// character.
88+
///
89+
/// - Precondition: `index` is a valid index in the receiver's trailing trivia collection.
90+
///
91+
/// - Parameters:
92+
/// - index: The index of the trivia piece in the trailing trivia whose location should be
93+
/// returned.
94+
/// - converter: The `SourceLocationConverter` that was previously initialized using the root
95+
/// tree of this node.
96+
/// - Returns: The source location of the trivia piece.
97+
func startLocation(
98+
ofTrailingTriviaAt index: Trivia.Index,
99+
converter: SourceLocationConverter
100+
) -> SourceLocation {
101+
return converter.location(for: position(ofTrailingTriviaAt: index))
102+
}
103+
104+
/// The collection of all contiguous trivia preceding this node; that is, the trailing trivia of
105+
/// the node before it and the leading trivia of the node itself.
106+
var allPrecedingTrivia: Trivia {
107+
var result: Trivia
108+
if let previousTrailingTrivia = previousToken(viewMode: .sourceAccurate)?.trailingTrivia {
109+
result = previousTrailingTrivia
110+
} else {
111+
result = Trivia()
112+
}
113+
result += leadingTrivia
114+
return result
115+
}
116+
117+
/// The collection of all contiguous trivia following this node; that is, the trailing trivia of
118+
/// the node and the leading trivia of the node after it.
119+
var allFollowingTrivia: Trivia {
120+
var result = trailingTrivia
121+
if let nextLeadingTrivia = nextToken(viewMode: .sourceAccurate)?.leadingTrivia {
122+
result += nextLeadingTrivia
123+
}
124+
return result
125+
}
126+
127+
/// Indicates whether the node has any preceding line comments.
128+
///
129+
/// Due to the way trivia is parsed, a preceding comment might be in either the leading trivia of
130+
/// the node or the trailing trivia of the previous token.
131+
var hasPrecedingLineComment: Bool {
132+
if let previousTrailingTrivia = previousToken(viewMode: .sourceAccurate)?.trailingTrivia,
133+
previousTrailingTrivia.hasLineComment
134+
{
135+
return true
136+
}
137+
return leadingTrivia.hasLineComment
138+
}
139+
140+
/// Indicates whether the node has any preceding comments of any kind.
141+
///
142+
/// Due to the way trivia is parsed, a preceding comment might be in either the leading trivia of
143+
/// the node or the trailing trivia of the previous token.
144+
var hasAnyPrecedingComment: Bool {
145+
if let previousTrailingTrivia = previousToken(viewMode: .sourceAccurate)?.trailingTrivia,
146+
previousTrailingTrivia.hasAnyComments
147+
{
148+
return true
149+
}
150+
return leadingTrivia.hasAnyComments
151+
}
59152
}
60153

61154
extension SyntaxCollection {

Sources/SwiftFormat/Core/Trivia+Convenience.swift

+8-6
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@ extension Trivia {
3434

3535
/// Returns this set of trivia, without any leading spaces.
3636
func withoutLeadingSpaces() -> Trivia {
37-
return Trivia(
38-
pieces: Array(drop {
39-
if case .spaces = $0 { return false }
40-
if case .tabs = $0 { return false }
41-
return true
42-
}))
37+
return Trivia(pieces: self.pieces.drop(while: \.isSpaceOrTab))
38+
}
39+
40+
func withoutTrailingSpaces() -> Trivia {
41+
guard let lastNonSpaceIndex = self.pieces.lastIndex(where: \.isSpaceOrTab) else {
42+
return self
43+
}
44+
return Trivia(pieces: self[..<lastNonSpaceIndex])
4345
}
4446

4547
/// Returns this trivia, excluding the last newline and anything following it.

0 commit comments

Comments
 (0)