1212
1313import struct Basics. AbsolutePath
1414import protocol Basics. FileSystem
15- import var Basics. localFileSystem
16- import struct Basics. SwiftVersion
1715
1816import struct SwiftDiagnostics. Diagnostic
1917import struct SwiftDiagnostics. DiagnosticCategory
@@ -44,7 +42,7 @@ private enum Error: Swift.Error {
4442}
4543
4644// FIXME: An abstraction for tests to work around missing memberwise initializers in `TSCUtility.SerializedDiagnostics`.
47- protocol AnySourceLocation {
45+ protocol AnySourceLocation : Hashable {
4846 var filename : String { get }
4947 var line : UInt64 { get }
5048 var column : UInt64 { get }
@@ -75,12 +73,128 @@ protocol AnyDiagnostic {
7573 var fixIts : [ FixIt ] { get }
7674}
7775
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+
7898extension 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+
80107extension SerializedDiagnostics . FixIt : AnyFixIt { }
81108
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+
82196/// The backing API for `SwiftFixitCommand`.
83- package struct SwiftFixIt /*: ~Copyable */ {
197+ package struct SwiftFixIt /*: ~Copyable */ { // TODO: Crashes with ~Copyable
84198 private typealias DiagnosticsPerFile = [ SourceFile : [ SwiftDiagnostics . Diagnostic ] ]
85199
86200 private let fileSystem : any FileSystem
@@ -96,7 +210,7 @@ package struct SwiftFixIt /*: ~Copyable */ {
96210 let diagnostics = try diagnosticFiles. map { path in
97211 let fileContents = try fileSystem. readFileContents ( path)
98212 return try TSCUtility . SerializedDiagnostics ( bytes: fileContents) . diagnostics
99- } . lazy . joined ( )
213+ } . joined ( )
100214
101215 self = try SwiftFixIt (
102216 diagnostics: diagnostics,
@@ -105,62 +219,50 @@ package struct SwiftFixIt /*: ~Copyable */ {
105219 )
106220 }
107221
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 > ,
111225 fileSystem: any FileSystem
112226 ) throws {
113227 self . fileSystem = fileSystem
114228
229+ var filter = PrimaryDiagnosticFilter < Diagnostic > ( categories: categories)
230+ _ = consume categories
231+
115232 // Build a map from source files to `SwiftDiagnostics` diagnostics.
116233 var diagnosticsPerFile : DiagnosticsPerFile = [ : ]
117-
118234 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- }
140235
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 )
145240
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) {
149249 continue
150250 }
151251
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 {
156256 continue
157257 }
158- }
159258
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+ }
161263 }
162264
163- self . diagnosticsPerFile = consume diagnosticsPerFile
265+ self . diagnosticsPerFile = diagnosticsPerFile
164266 }
165267
166268 package func applyFixIts( ) throws {
@@ -281,8 +383,8 @@ extension SourceFile: Hashable {
281383 }
282384}
283385
284- private struct DiagnosticConverter /* : ~Copyable */ {
285- private struct SourceFileCache /* : ~Copyable */ {
386+ private struct DiagnosticConverter : ~ Copyable {
387+ private struct SourceFileCache : ~ Copyable {
286388 private let fileSystem : any FileSystem
287389
288390 private var sourceFiles : [ AbsolutePath : SourceFile ]
@@ -323,7 +425,7 @@ extension DiagnosticConverter {
323425 // emit notes with those fix-its.
324426 private static func fixIt(
325427 from diagnostic: borrowing some AnyDiagnostic ,
326- in sourceFile: /*borrowing*/ SourceFile
428+ in sourceFile: /* borrowing */ SourceFile
327429 ) throws -> SwiftDiagnostics . FixIt {
328430 let changes = try diagnostic. fixIts. map { fixIt in
329431 let startPosition = try sourceFile. position ( of: fixIt. start)
@@ -341,7 +443,7 @@ extension DiagnosticConverter {
341443
342444 private static func highlights(
343445 from diagnostic: borrowing some AnyDiagnostic ,
344- in sourceFile: /*borrowing*/ SourceFile
446+ in sourceFile: /* borrowing */ SourceFile
345447 ) throws -> [ Syntax ] {
346448 try diagnostic. ranges. map { startLocation, endLocation in
347449 let startPosition = try sourceFile. position ( of: startLocation)
@@ -376,19 +478,19 @@ extension DiagnosticConverter {
376478
377479 mutating func diagnostic(
378480 from diagnostic: borrowing some AnyDiagnostic
379- ) throws -> Diagnostic ? {
380- if diagnostic. fixIts. isEmpty {
481+ ) throws -> Diagnostic {
482+ guard ! diagnostic. fixIts. isEmpty else {
381483 preconditionFailure ( " Expected diagnostic with fix-its " )
382484 }
383485
384486 guard let location = diagnostic. location else {
385- return nil
487+ preconditionFailure ( " Diagnostic without location cannot be converted " )
386488 }
387489
388490 let message : DeserializedDiagnosticMessage
389491 do {
390492 guard let severity = SwiftDiagnostics . DiagnosticSeverity ( from: diagnostic. level) else {
391- return nil
493+ preconditionFailure ( " Diagnostic with 'ignored' severity cannot be converted " )
392494 }
393495
394496 let category : SwiftDiagnostics . DiagnosticCategory ? =
0 commit comments