12
12
13
13
import struct Basics. AbsolutePath
14
14
import protocol Basics. FileSystem
15
- import var Basics. localFileSystem
16
- import struct Basics. SwiftVersion
17
15
18
16
import struct SwiftDiagnostics. Diagnostic
19
17
import struct SwiftDiagnostics. DiagnosticCategory
@@ -44,7 +42,7 @@ private enum Error: Swift.Error {
44
42
}
45
43
46
44
// FIXME: An abstraction for tests to work around missing memberwise initializers in `TSCUtility.SerializedDiagnostics`.
47
- protocol AnySourceLocation {
45
+ protocol AnySourceLocation : Hashable {
48
46
var filename : String { get }
49
47
var line : UInt64 { get }
50
48
var column : UInt64 { get }
@@ -75,12 +73,128 @@ protocol AnyDiagnostic {
75
73
var fixIts : [ FixIt ] { get }
76
74
}
77
75
76
+ extension AnyDiagnostic {
77
+ var isPrimary : Bool {
78
+ self . level != . note
79
+ }
80
+
81
+ var isNote : Bool {
82
+ !self . isPrimary
83
+ }
84
+
85
+ var isIgnored : Bool {
86
+ self . level == . ignored
87
+ }
88
+
89
+ var hasFixIt : Bool {
90
+ !self . fixIts. isEmpty
91
+ }
92
+
93
+ var hasNoLocation : Bool {
94
+ self . location == nil
95
+ }
96
+ }
97
+
78
98
extension SerializedDiagnostics . Diagnostic : AnyDiagnostic { }
79
- extension SerializedDiagnostics . SourceLocation : AnySourceLocation { }
99
+ extension SerializedDiagnostics . SourceLocation : AnySourceLocation , @retroactive Hashable {
100
+ public func hash( into hasher: inout Hasher ) {
101
+ hasher. combine ( self . filename)
102
+ hasher. combine ( self . line)
103
+ hasher. combine ( self . column)
104
+ }
105
+ }
106
+
80
107
extension SerializedDiagnostics . FixIt : AnyFixIt { }
81
108
109
+ /// Encapsulates initial diagnostic skipping behavior.
110
+ private struct PrimaryDiagnosticFilter < Diagnostic: AnyDiagnostic > : ~ Copyable {
111
+ /// A hashable type storing the minimum data necessary to uniquely identify
112
+ /// a diagnostic for our purposes.
113
+ private struct DiagnosticID : Hashable {
114
+ private let location : Diagnostic . SourceLocation
115
+ private let message : String
116
+ private let level : SerializedDiagnostics . Diagnostic . Level
117
+
118
+ init ( diagnostic: Diagnostic ) {
119
+ self . level = diagnostic. level
120
+ self . message = diagnostic. text
121
+ // Force the location. We should be filtering out diagnostics
122
+ // without a location.
123
+ self . location = diagnostic. location!
124
+ }
125
+ }
126
+
127
+ private var uniquePrimaryDiagnostics : Set < DiagnosticID > = [ ]
128
+
129
+ let categories : Set < String >
130
+
131
+ init ( categories: Set < String > ) {
132
+ self . categories = categories
133
+ }
134
+
135
+ /// Returns a Boolean value indicating whether to skip the given primary
136
+ /// diagnostic and its notes.
137
+ mutating func shouldSkip( primaryDiagnosticWithNotes: some Collection < Diagnostic > ) -> Bool {
138
+ let diagnostic = primaryDiagnosticWithNotes [ primaryDiagnosticWithNotes. startIndex]
139
+ precondition ( diagnostic. isPrimary)
140
+
141
+ // Skip if ignored.
142
+ if diagnostic. isIgnored {
143
+ return true
144
+ }
145
+
146
+ // Skip if no location.
147
+ if diagnostic. hasNoLocation {
148
+ return true
149
+ }
150
+
151
+ // Skip if categories are given and the diagnostic does not
152
+ // belong to any of them.
153
+ if !self . categories. isEmpty {
154
+ guard let category = diagnostic. category, self . categories. contains ( category) else {
155
+ return true
156
+ }
157
+ }
158
+
159
+ let notes = primaryDiagnosticWithNotes. dropFirst ( )
160
+ precondition ( notes. allSatisfy ( \. isNote) )
161
+
162
+ // Consider the diagnostic compromised if a note does not have a
163
+ // location.
164
+ if notes. contains ( where: \. hasNoLocation) {
165
+ return true
166
+ }
167
+
168
+ switch notes. count ( where: \. hasFixIt) {
169
+ case 0 :
170
+ // No notes have fix-its. Skip if neither does the primary
171
+ // diagnostic.
172
+ guard diagnostic. hasFixIt else {
173
+ return true
174
+ }
175
+ case 1 :
176
+ break
177
+ default :
178
+ // Skip if more than 1 note has a fix-it. These diagnostics
179
+ // generally require user intervention.
180
+ // TODO: This will have to be done lazier once we support printing them.
181
+ return true
182
+ }
183
+
184
+ // Skip if we've seen this primary diagnostic before.
185
+ //
186
+ // NB: This check is done last to prevent the set from growing without
187
+ // need.
188
+ guard self . uniquePrimaryDiagnostics. insert ( . init( diagnostic: diagnostic) ) . inserted else {
189
+ return true
190
+ }
191
+
192
+ return false
193
+ }
194
+ }
195
+
82
196
/// The backing API for `SwiftFixitCommand`.
83
- package struct SwiftFixIt /*: ~Copyable */ {
197
+ package struct SwiftFixIt /*: ~Copyable */ { // TODO: Crashes with ~Copyable
84
198
private typealias DiagnosticsPerFile = [ SourceFile : [ SwiftDiagnostics . Diagnostic ] ]
85
199
86
200
private let fileSystem : any FileSystem
@@ -96,7 +210,7 @@ package struct SwiftFixIt /*: ~Copyable */ {
96
210
let diagnostics = try diagnosticFiles. map { path in
97
211
let fileContents = try fileSystem. readFileContents ( path)
98
212
return try TSCUtility . SerializedDiagnostics ( bytes: fileContents) . diagnostics
99
- } . lazy . joined ( )
213
+ } . joined ( )
100
214
101
215
self = try SwiftFixIt (
102
216
diagnostics: diagnostics,
@@ -105,62 +219,50 @@ package struct SwiftFixIt /*: ~Copyable */ {
105
219
)
106
220
}
107
221
108
- init (
109
- diagnostics: some Collection < some AnyDiagnostic > ,
110
- categories: Set < String > = [ ] ,
222
+ init < Diagnostic : AnyDiagnostic > (
223
+ diagnostics: some Collection < Diagnostic > ,
224
+ categories: Set < String > ,
111
225
fileSystem: any FileSystem
112
226
) throws {
113
227
self . fileSystem = fileSystem
114
228
229
+ var filter = PrimaryDiagnosticFilter < Diagnostic > ( categories: categories)
230
+ _ = consume categories
231
+
115
232
// Build a map from source files to `SwiftDiagnostics` diagnostics.
116
233
var diagnosticsPerFile : DiagnosticsPerFile = [ : ]
117
-
118
234
var diagnosticConverter = DiagnosticConverter ( fileSystem: fileSystem)
119
- var currentPrimaryDiagnosticHasNoteWithFixIt = false
120
-
121
- for diagnostic in diagnostics {
122
- let hasFixits = !diagnostic. fixIts. isEmpty
123
-
124
- if diagnostic. level == . note {
125
- if hasFixits {
126
- // The Swift compiler produces parallel fix-its by attaching
127
- // them to notes, which in turn associate to a single
128
- // error/warning. Prefer the first fix-it in this case: if
129
- // the last error/warning we saw has a note with a fix-it
130
- // and so is this one, skip it.
131
- if currentPrimaryDiagnosticHasNoteWithFixIt {
132
- continue
133
- }
134
-
135
- currentPrimaryDiagnosticHasNoteWithFixIt = true
136
- }
137
- } else {
138
- currentPrimaryDiagnosticHasNoteWithFixIt = false
139
- }
140
235
141
- // We are only interested in diagnostics with fix-its.
142
- guard hasFixits else {
143
- continue
144
- }
236
+ var nextPrimaryIndex = diagnostics. startIndex
237
+ while nextPrimaryIndex != diagnostics . endIndex {
238
+ let currentPrimaryIndex = nextPrimaryIndex
239
+ precondition ( diagnostics [ currentPrimaryIndex ] . isPrimary )
145
240
146
- guard let ( sourceFile, convertedDiagnostic) =
147
- try diagnosticConverter. diagnostic ( from: diagnostic)
148
- else {
241
+ // Shift the index to the next primary diagnostic.
242
+ repeat {
243
+ diagnostics. formIndex ( after: & nextPrimaryIndex)
244
+ } while nextPrimaryIndex != diagnostics. endIndex && diagnostics [ nextPrimaryIndex] . isNote
245
+
246
+ let primaryDiagnosticWithNotes = diagnostics [ currentPrimaryIndex ..< nextPrimaryIndex]
247
+
248
+ if filter. shouldSkip ( primaryDiagnosticWithNotes: primaryDiagnosticWithNotes) {
149
249
continue
150
250
}
151
251
152
- if !categories . isEmpty {
153
- guard let category = convertedDiagnostic . diagMessage . category ? . name ,
154
- categories . contains ( category )
155
- else {
252
+ for diagnostic in primaryDiagnosticWithNotes {
253
+ // We are only interested in diagnostics with fix-its.
254
+ // TODO: This will have to change once we support printing them.
255
+ guard diagnostic . hasFixIt else {
156
256
continue
157
257
}
158
- }
159
258
160
- diagnosticsPerFile [ consume sourceFile, default: [ ] ] . append ( consume convertedDiagnostic)
259
+ let ( sourceFile, convertedDiagnostic) = try diagnosticConverter. diagnostic ( from: diagnostic)
260
+
261
+ diagnosticsPerFile [ consume sourceFile, default: [ ] ] . append ( consume convertedDiagnostic)
262
+ }
161
263
}
162
264
163
- self . diagnosticsPerFile = consume diagnosticsPerFile
265
+ self . diagnosticsPerFile = diagnosticsPerFile
164
266
}
165
267
166
268
package func applyFixIts( ) throws {
@@ -281,8 +383,8 @@ extension SourceFile: Hashable {
281
383
}
282
384
}
283
385
284
- private struct DiagnosticConverter /* : ~Copyable */ {
285
- private struct SourceFileCache /* : ~Copyable */ {
386
+ private struct DiagnosticConverter : ~ Copyable {
387
+ private struct SourceFileCache : ~ Copyable {
286
388
private let fileSystem : any FileSystem
287
389
288
390
private var sourceFiles : [ AbsolutePath : SourceFile ]
@@ -323,7 +425,7 @@ extension DiagnosticConverter {
323
425
// emit notes with those fix-its.
324
426
private static func fixIt(
325
427
from diagnostic: borrowing some AnyDiagnostic ,
326
- in sourceFile: /*borrowing*/ SourceFile
428
+ in sourceFile: /* borrowing */ SourceFile
327
429
) throws -> SwiftDiagnostics . FixIt {
328
430
let changes = try diagnostic. fixIts. map { fixIt in
329
431
let startPosition = try sourceFile. position ( of: fixIt. start)
@@ -341,7 +443,7 @@ extension DiagnosticConverter {
341
443
342
444
private static func highlights(
343
445
from diagnostic: borrowing some AnyDiagnostic ,
344
- in sourceFile: /*borrowing*/ SourceFile
446
+ in sourceFile: /* borrowing */ SourceFile
345
447
) throws -> [ Syntax ] {
346
448
try diagnostic. ranges. map { startLocation, endLocation in
347
449
let startPosition = try sourceFile. position ( of: startLocation)
@@ -376,19 +478,19 @@ extension DiagnosticConverter {
376
478
377
479
mutating func diagnostic(
378
480
from diagnostic: borrowing some AnyDiagnostic
379
- ) throws -> Diagnostic ? {
380
- if diagnostic. fixIts. isEmpty {
481
+ ) throws -> Diagnostic {
482
+ guard ! diagnostic. fixIts. isEmpty else {
381
483
preconditionFailure ( " Expected diagnostic with fix-its " )
382
484
}
383
485
384
486
guard let location = diagnostic. location else {
385
- return nil
487
+ preconditionFailure ( " Diagnostic without location cannot be converted " )
386
488
}
387
489
388
490
let message : DeserializedDiagnosticMessage
389
491
do {
390
492
guard let severity = SwiftDiagnostics . DiagnosticSeverity ( from: diagnostic. level) else {
391
- return nil
493
+ preconditionFailure ( " Diagnostic with 'ignored' severity cannot be converted " )
392
494
}
393
495
394
496
let category : SwiftDiagnostics . DiagnosticCategory ? =
0 commit comments