Skip to content

Commit 0258d1d

Browse files
authored
Merge pull request #2007 from ahoppen/dont-cache-failed-diag-reports
Do not cache diagnostic reports for sourcekitd requests that have failed
2 parents 66793ff + abea29c commit 0258d1d

File tree

2 files changed

+66
-12
lines changed

2 files changed

+66
-12
lines changed

Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ import SwiftExtensions
2020
import SwiftParserDiagnostics
2121

2222
actor DiagnosticReportManager {
23-
/// A task to produce diagnostics, either from a diagnostics request to `sourcektid` or by using the built-in swift-syntax.
24-
private typealias ReportTask = RefCountedCancellableTask<RelatedFullDocumentDiagnosticReport>
23+
/// A task to produce diagnostics, either from a diagnostics request to `sourcekitd` or by using the built-in swift-syntax.
24+
private typealias ReportTask = RefCountedCancellableTask<
25+
(report: RelatedFullDocumentDiagnosticReport, cachable: Bool)
26+
>
2527

2628
private let sourcekitd: SourceKitD
2729
private let options: SourceKitLSPOptions
@@ -68,7 +70,14 @@ actor DiagnosticReportManager {
6870
buildSettings: SwiftCompileCommand?
6971
) async throws -> RelatedFullDocumentDiagnosticReport {
7072
if let reportTask = reportTask(for: snapshot.id, buildSettings: buildSettings), await !reportTask.isCancelled {
71-
return try await reportTask.value
73+
do {
74+
let cachedValue = try await reportTask.value
75+
if cachedValue.cachable {
76+
return cachedValue.report
77+
}
78+
} catch {
79+
// Do not cache failed requests
80+
}
7281
}
7382
let reportTask: ReportTask
7483
if let buildSettings, !buildSettings.isFallback {
@@ -88,7 +97,7 @@ actor DiagnosticReportManager {
8897
}
8998
}
9099
setReportTask(for: snapshot.id, buildSettings: buildSettings, reportTask: reportTask)
91-
return try await reportTask.value
100+
return try await reportTask.value.report
92101
}
93102

94103
func removeItemsFromCache(with uri: DocumentURI) async {
@@ -98,7 +107,7 @@ actor DiagnosticReportManager {
98107
private func requestReport(
99108
with snapshot: DocumentSnapshot,
100109
compilerArgs: [String]
101-
) async throws -> LanguageServerProtocol.RelatedFullDocumentDiagnosticReport {
110+
) async throws -> (report: RelatedFullDocumentDiagnosticReport, cachable: Bool) {
102111
try Task.checkCancellation()
103112

104113
let keys = self.keys
@@ -119,17 +128,23 @@ actor DiagnosticReportManager {
119128
)
120129
} catch SKDError.requestFailed(let sourcekitdError) {
121130
var errorMessage = sourcekitdError
131+
if errorMessage.contains("semantic editor is disabled") {
132+
throw SKDError.requestFailed(sourcekitdError)
133+
}
122134
if errorMessage.hasPrefix("error response (Request Failed): error: ") {
123135
errorMessage = String(errorMessage.dropFirst(40))
124136
}
125-
return RelatedFullDocumentDiagnosticReport(items: [
137+
let report = RelatedFullDocumentDiagnosticReport(items: [
126138
Diagnostic(
127139
range: Position(line: 0, utf16index: 0)..<Position(line: 0, utf16index: 0),
128140
severity: .error,
129141
source: "SourceKit",
130142
message: "Internal SourceKit error: \(errorMessage)"
131143
)
132144
])
145+
// If generating the diagnostic report failed because of a sourcekitd problem, mark as as non-cachable because
146+
// executing the sourcekitd request again might succeed (eg. if sourcekitd has been restored after a crash).
147+
return (report, cachable: false)
133148
}
134149

135150
try Task.checkCancellation()
@@ -144,12 +159,13 @@ actor DiagnosticReportManager {
144159
)
145160
}) ?? []
146161

147-
return RelatedFullDocumentDiagnosticReport(items: diagnostics)
162+
let report = RelatedFullDocumentDiagnosticReport(items: diagnostics)
163+
return (report, cachable: true)
148164
}
149165

150166
private func requestFallbackReport(
151167
with snapshot: DocumentSnapshot
152-
) async throws -> LanguageServerProtocol.RelatedFullDocumentDiagnosticReport {
168+
) async throws -> (report: RelatedFullDocumentDiagnosticReport, cachable: Bool) {
153169
// If we don't have build settings or we only have fallback build settings,
154170
// sourcekitd won't be able to give us accurate semantic diagnostics.
155171
// Fall back to providing syntactic diagnostics from the built-in
@@ -163,7 +179,8 @@ actor DiagnosticReportManager {
163179
}
164180
return Diagnostic(diag, in: snapshot)
165181
}
166-
return RelatedFullDocumentDiagnosticReport(items: diagnostics)
182+
let report = RelatedFullDocumentDiagnosticReport(items: diagnostics)
183+
return (report, cachable: true)
167184
}
168185

169186
/// The reportTask for the given document snapshot and buildSettings.
@@ -184,10 +201,10 @@ actor DiagnosticReportManager {
184201
buildSettings: SwiftCompileCommand?,
185202
reportTask: ReportTask
186203
) {
187-
reportTaskCache.insert((snapshotID, buildSettings, reportTask), at: 0)
188-
189204
// Remove any reportTasks for old versions of this document.
190-
reportTaskCache.removeAll(where: { $0.snapshotID < snapshotID })
205+
reportTaskCache.removeAll(where: { $0.snapshotID <= snapshotID })
206+
207+
reportTaskCache.insert((snapshotID, buildSettings, reportTask), at: 0)
191208

192209
// If we still have more than `cacheSize` reportTasks, delete the ones that
193210
// were produced last. We can always re-request them on-demand.

Tests/SourceKitDTests/CrashRecoveryTests.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,4 +312,41 @@ final class CrashRecoveryTests: XCTestCase {
312312
"Clangd restarted too quickly after crashing twice in a row. We are not preventing crash loops."
313313
)
314314
}
315+
316+
func testPullDiagnosticsAfterCrash() async throws {
317+
try SkipUnless.platformIsDarwin("Linux and Windows use in-process sourcekitd")
318+
try SkipUnless.longTestsEnabled()
319+
320+
// Ensure that we don't cache the diagnostic report from the crashed sourcekitd and don't emit any of the internal
321+
// sourcekitd errors such as `semantic editor disabled` in the diagnostic report.
322+
let testClient = try await TestSourceKitLSPClient()
323+
let uri = DocumentURI(for: .swift)
324+
testClient.openDocument(
325+
"""
326+
func test() {
327+
let x: String = 1
328+
}
329+
""",
330+
uri: uri
331+
)
332+
333+
let swiftLanguageService =
334+
await testClient.server.languageService(
335+
for: uri,
336+
.swift,
337+
in: testClient.server.workspaceForDocument(uri: uri)!
338+
) as! SwiftLanguageService
339+
340+
await swiftLanguageService.crash()
341+
342+
try await repeatUntilExpectedResult(timeout: .seconds(30)) {
343+
let diagnostics = try await testClient.send(DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri)))
344+
if diagnostics.fullReport?.items.count == 0 {
345+
return false
346+
}
347+
let item = try XCTUnwrap(diagnostics.fullReport?.items.only)
348+
XCTAssertEqual(item.message, "Cannot convert value of type 'Int' to specified type 'String'")
349+
return true
350+
}
351+
}
315352
}

0 commit comments

Comments
 (0)