Skip to content

Commit 236ff1b

Browse files
committed
Handle diagnostics in secondary files correctly
rdar://130503535
1 parent 9c6a91d commit 236ff1b

File tree

3 files changed

+68
-4
lines changed

3 files changed

+68
-4
lines changed

Sources/SourceKitLSP/Swift/Diagnostic.swift

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,16 +154,28 @@ fileprivate extension String {
154154
extension Diagnostic {
155155

156156
/// Creates a diagnostic from a sourcekitd response dictionary.
157+
///
158+
/// `snapshot` is the snapshot of the document for which the diagnostics are generated.
159+
/// `documentManager` is used to resolve positions of notes in secondary files.
157160
init?(
158161
_ diag: SKDResponseDictionary,
159162
in snapshot: DocumentSnapshot,
163+
documentManager: DocumentManager,
160164
useEducationalNoteAsCode: Bool
161165
) {
162-
// FIXME: this assumes that the diagnostics are all in the same file.
163-
164166
let keys = diag.sourcekitd.keys
165167
let values = diag.sourcekitd.values
166168

169+
guard let filePath: String = diag[keys.filePath] else {
170+
logger.fault("Missing file path in diagnostic")
171+
return nil
172+
}
173+
let uri = DocumentURI(filePath: filePath, isDirectory: false)
174+
guard uri == snapshot.uri else {
175+
logger.error("Ignoring diagnostic from a different file: \(uri)")
176+
return nil
177+
}
178+
167179
guard let message: String = diag[keys.description]?.withFirstLetterUppercased() else { return nil }
168180

169181
var range: Range<Position>? = nil
@@ -237,7 +249,13 @@ extension Diagnostic {
237249
if let sknotes: SKDResponseArray = diag[keys.diagnostics] {
238250
notes = []
239251
sknotes.forEach { (_, sknote) -> Bool in
240-
guard let note = DiagnosticRelatedInformation(sknote, in: snapshot) else { return true }
252+
guard
253+
let note = DiagnosticRelatedInformation(
254+
sknote,
255+
primaryDocumentSnapshot: snapshot,
256+
documentManager: documentManager
257+
)
258+
else { return true }
241259
notes?.append(note)
242260
return true
243261
}
@@ -309,9 +327,28 @@ extension Diagnostic {
309327
extension DiagnosticRelatedInformation {
310328

311329
/// Creates related information from a sourcekitd note response dictionary.
312-
init?(_ diag: SKDResponseDictionary, in snapshot: DocumentSnapshot) {
330+
///
331+
/// `primaryDocumentSnapshot` is the snapshot of the document for which the diagnostics are generated.
332+
/// `documentManager` is used to resolve positions of notes in secondary files.
333+
init?(_ diag: SKDResponseDictionary, primaryDocumentSnapshot: DocumentSnapshot, documentManager: DocumentManager) {
313334
let keys = diag.sourcekitd.keys
314335

336+
guard let filePath: String = diag[keys.filePath] else {
337+
logger.fault("Missing file path in related diagnostic information")
338+
return nil
339+
}
340+
let uri = DocumentURI(filePath: filePath, isDirectory: false)
341+
let snapshot: DocumentSnapshot
342+
if uri == primaryDocumentSnapshot.uri {
343+
snapshot = primaryDocumentSnapshot
344+
} else if let inMemorySnapshot = try? documentManager.latestSnapshot(uri) {
345+
snapshot = inMemorySnapshot
346+
} else if let snapshotFromDisk = try? DocumentSnapshot(withContentsFromDisk: uri, language: .swift) {
347+
snapshot = snapshotFromDisk
348+
} else {
349+
return nil
350+
}
351+
315352
var position: Position? = nil
316353
if let line: Int = diag[keys.line],
317354
let utf8Column: Int = diag[keys.column],

Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ actor DiagnosticReportManager {
113113
Diagnostic(
114114
diag,
115115
in: snapshot,
116+
documentManager: documentManager,
116117
useEducationalNoteAsCode: self.clientHasDiagnosticsCodeDescriptionSupport
117118
)
118119
}) ?? []

Tests/SourceKitLSPTests/PullDiagnosticsTests.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,4 +349,30 @@ final class PullDiagnosticsTests: XCTestCase {
349349
diagnosticRequestCancelled.fulfill()
350350
try await fulfillmentOfOrThrow([diagnosticResponseReceived])
351351
}
352+
353+
func testNoteInSecondaryFile() async throws {
354+
let project = try await SwiftPMTestProject(files: [
355+
"FileA.swift": """
356+
@available(*, unavailable)
357+
struct 1️⃣Test {}
358+
""",
359+
"FileB.swift": """
360+
func test() {
361+
_ = Test()
362+
}
363+
""",
364+
])
365+
366+
let (uri, _) = try project.openDocument("FileB.swift")
367+
let diagnostics = try await project.testClient.send(
368+
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
369+
)
370+
guard case .full(let diagnostics) = diagnostics else {
371+
XCTFail("Expected full diagnostics report")
372+
return
373+
}
374+
let diagnostic = try XCTUnwrap(diagnostics.items.only)
375+
let note = try XCTUnwrap(diagnostic.relatedInformation?.only)
376+
XCTAssertEqual(note.location, try project.location(from: "1️⃣", to: "1️⃣", in: "FileA.swift"))
377+
}
352378
}

0 commit comments

Comments
 (0)