12
12
13
13
import LSPLogging
14
14
import LanguageServerProtocol
15
+ import SourceKitD
16
+ import SwiftParserDiagnostics
15
17
16
- protocol DiagnosticReportManagerDelegate : AnyObject {
17
- /// Notify the delegate that compute a report with a given snapshot
18
- func reportWithSnapshotReceived( _ snapshot: DocumentSnapshot , compilerArgs: [ String ] ) async throws
19
- -> RelatedFullDocumentDiagnosticReport
18
+ actor DiagnosticReportManager {
19
+ private let sourcekitd : SourceKitD
20
+ private let syntaxTreeManager : SyntaxTreeManager
21
+ private let documentManager : DocumentManager
22
+ private let clientHasDiagnosticsCodeDescriptionSupport : Bool
20
23
21
- /// Notify the delegate that compute a report with a given snapshot for fallback
22
- func fallbackReportWithSnapshotReceived( _ snapshot: DocumentSnapshot ) async throws
23
- -> RelatedFullDocumentDiagnosticReport
24
- }
24
+ private nonisolated var keys : sourcekitd_keys { return sourcekitd. keys }
25
+ private nonisolated var requests : sourcekitd_requests { return sourcekitd. requests }
25
26
26
- actor DiagnosticReportManager {
27
27
/// The cache that stores reports for snapshot ids
28
28
///
29
29
/// Conceptually, this is a dictionary. To prevent excessive memory usage we
@@ -40,19 +40,21 @@ actor DiagnosticReportManager {
40
40
/// - Note: This has been chosen without scientific measurements.
41
41
private let cacheSize = 5
42
42
43
- /// Delegate to handle any DiagnosticReportManager events.
44
- private weak var delegate : DiagnosticReportManagerDelegate ?
45
-
46
- func setDelegate( _ delegate: DiagnosticReportManagerDelegate ? ) async {
47
- self . delegate = delegate
43
+ init (
44
+ sourcekitd: SourceKitD ,
45
+ syntaxTreeManager: SyntaxTreeManager ,
46
+ documentManager: DocumentManager ,
47
+ clientHasDiagnosticsCodeDescriptionSupport: Bool
48
+ ) {
49
+ self . sourcekitd = sourcekitd
50
+ self . syntaxTreeManager = syntaxTreeManager
51
+ self . documentManager = documentManager
52
+ self . clientHasDiagnosticsCodeDescriptionSupport = clientHasDiagnosticsCodeDescriptionSupport
48
53
}
49
54
50
55
func diagnosticReport( for snapshot: DocumentSnapshot , buildSettings: SwiftCompileCommand ? , useCache: Bool = false )
51
56
async throws -> RelatedFullDocumentDiagnosticReport
52
57
{
53
- guard let delegate = self . delegate else {
54
- throw ResponseError . unknown ( " DiagnosticReportManagerDelegate should not be nil " )
55
- }
56
58
if useCache, let report = report ( for: snapshot. id) {
57
59
return report
58
60
}
@@ -64,26 +66,83 @@ actor DiagnosticReportManager {
64
66
// sourcekitd won't be able to give us accurate semantic diagnostics.
65
67
// Fall back to providing syntactic diagnostics from the built-in
66
68
// swift-syntax. That's the best we can do for now.
67
- let report = try await delegate . fallbackReportWithSnapshotReceived ( snapshot)
69
+ let report = try await fallbackReport ( with : snapshot)
68
70
setReport ( for: snapshot. id, report: report)
69
71
return report
70
72
}
71
73
72
- let report = try await delegate . reportWithSnapshotReceived ( snapshot, compilerArgs: buildSettings. compilerArgs)
74
+ let report = try await report ( with : snapshot, compilerArgs: buildSettings. compilerArgs)
73
75
setReport ( for: snapshot. id, report: report)
74
76
return report
75
77
}
78
+ }
79
+
80
+ private extension DiagnosticReportManager {
81
+ func report( with snapshot: DocumentSnapshot , compilerArgs: [ String ] ) async throws
82
+ -> LanguageServerProtocol . RelatedFullDocumentDiagnosticReport
83
+ {
84
+ try Task . checkCancellation ( )
85
+
86
+ let keys = self . keys
87
+
88
+ let skreq = SKDRequestDictionary ( sourcekitd: self . sourcekitd)
89
+ skreq [ keys. request] = requests. diagnostics
90
+ skreq [ keys. sourcefile] = snapshot. uri. pseudoPath
91
+
92
+ // FIXME: SourceKit should probably cache this for us.
93
+ skreq [ keys. compilerargs] = compilerArgs
94
+
95
+ let dict = try await self . sourcekitd. send ( skreq, fileContents: snapshot. text)
96
+
97
+ try Task . checkCancellation ( )
98
+ guard ( try ? documentManager. latestSnapshot ( snapshot. uri) . id) == snapshot. id else {
99
+ // Check that the document wasn't modified while we were getting diagnostics. This could happen because we are
100
+ // calling `fullDocumentDiagnosticReport` from `publishDiagnosticsIfNeeded` outside of `messageHandlingQueue`
101
+ // and thus a concurrent edit is possible while we are waiting for the sourcekitd request to return a result.
102
+ throw ResponseError . unknown ( " Document was modified while loading document " )
103
+ }
104
+
105
+ let supportsCodeDescription = self . clientHasDiagnosticsCodeDescriptionSupport
106
+ var diagnostics : [ Diagnostic ] = [ ]
107
+ dict [ keys. diagnostics] ? . forEach { _, diag in
108
+ if let diag = Diagnostic ( diag, in: snapshot, useEducationalNoteAsCode: supportsCodeDescription) {
109
+ diagnostics. append ( diag)
110
+ }
111
+ return true
112
+ }
113
+
114
+ return RelatedFullDocumentDiagnosticReport ( items: diagnostics)
115
+ }
116
+
117
+ func fallbackReport( with snapshot: DocumentSnapshot ) async throws
118
+ -> LanguageServerProtocol . RelatedFullDocumentDiagnosticReport
119
+ {
120
+ // If we don't have build settings or we only have fallback build settings,
121
+ // sourcekitd won't be able to give us accurate semantic diagnostics.
122
+ // Fall back to providing syntactic diagnostics from the built-in
123
+ // swift-syntax. That's the best we can do for now.
124
+ let syntaxTree = await syntaxTreeManager. syntaxTree ( for: snapshot)
125
+ let swiftSyntaxDiagnostics = ParseDiagnosticsGenerator . diagnostics ( for: syntaxTree)
126
+ let diagnostics = swiftSyntaxDiagnostics. compactMap { ( diag) -> Diagnostic ? in
127
+ if diag. diagnosticID == StaticTokenError . editorPlaceholder. diagnosticID {
128
+ // Ignore errors about editor placeholders in the source file, similar to how sourcekitd ignores them.
129
+ return nil
130
+ }
131
+ return Diagnostic ( diag, in: snapshot)
132
+ }
133
+ return RelatedFullDocumentDiagnosticReport ( items: diagnostics)
134
+ }
76
135
77
136
/// The report for the given document snapshot.
78
- private func report( for snapshotID: DocumentSnapshot . ID ) -> RelatedFullDocumentDiagnosticReport ? {
137
+ func report( for snapshotID: DocumentSnapshot . ID ) -> RelatedFullDocumentDiagnosticReport ? {
79
138
return reportCache. first ( where: { $0. snapshotID == snapshotID } ) ? . report
80
139
}
81
140
82
141
/// Set the report for the given document snapshot.
83
142
///
84
143
/// If we are already storing `cacheSize` many reports, the oldest one
85
144
/// will get discarded.
86
- private func setReport( for snapshotID: DocumentSnapshot . ID , report: RelatedFullDocumentDiagnosticReport ) {
145
+ func setReport( for snapshotID: DocumentSnapshot . ID , report: RelatedFullDocumentDiagnosticReport ) {
87
146
reportCache. insert ( ( snapshotID, report) , at: 0 )
88
147
89
148
// Remove any reports for old versions of this document.
0 commit comments