Skip to content

Commit 4ed6239

Browse files
authored
Merge pull request #1162 from ahoppen/fewer-fatal-errors
Replace usages of `fatalError` that are recoverable by non-fatal error propagation constructs
2 parents 45763eb + e3bc561 commit 4ed6239

File tree

15 files changed

+151
-79
lines changed

15 files changed

+151
-79
lines changed

Sources/LanguageServerProtocol/Requests/IndexedRenameRequest.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ extension IndexedRenameRequest: Codable {
7272
)
7373
self.oldName = try container.decode(String.self, forKey: IndexedRenameRequest.CodingKeys.oldName)
7474
self.newName = try container.decode(String.self, forKey: IndexedRenameRequest.CodingKeys.newName)
75-
self.positions = try container.decode([String: [Position]].self, forKey: .positions).mapKeys(DocumentURI.init)
75+
self.positions = try container.decode([String: [Position]].self, forKey: .positions).compactMapKeys {
76+
try? DocumentURI(string: $0)
77+
}
7678
}
7779

7880
public func encode(to encoder: Encoder) throws {
@@ -81,13 +83,22 @@ extension IndexedRenameRequest: Codable {
8183
try container.encode(self.textDocument, forKey: IndexedRenameRequest.CodingKeys.textDocument)
8284
try container.encode(self.oldName, forKey: IndexedRenameRequest.CodingKeys.oldName)
8385
try container.encode(self.newName, forKey: IndexedRenameRequest.CodingKeys.newName)
84-
try container.encode(self.positions.mapKeys(\.stringValue), forKey: IndexedRenameRequest.CodingKeys.positions)
86+
try container.encode(
87+
self.positions.compactMapKeys { $0.stringValue },
88+
forKey: IndexedRenameRequest.CodingKeys.positions
89+
)
8590

8691
}
8792
}
8893

8994
fileprivate extension Dictionary {
90-
func mapKeys<NewKeyType: Hashable>(_ transform: (Key) -> NewKeyType) -> [NewKeyType: Value] {
91-
return [NewKeyType: Value](uniqueKeysWithValues: self.map { (transform($0.key), $0.value) })
95+
func compactMapKeys<NewKeyType: Hashable>(_ transform: (Key) -> NewKeyType?) -> [NewKeyType: Value] {
96+
let newKeysAndValues = self.compactMap { (key, value) -> (NewKeyType, Value)? in
97+
guard let newKey = transform(key) else {
98+
return nil
99+
}
100+
return (newKey, value)
101+
}
102+
return [NewKeyType: Value](uniqueKeysWithValues: newKeysAndValues)
92103
}
93104
}

Sources/LanguageServerProtocol/SupportTypes/DocumentURI.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@
1212

1313
import Foundation
1414

15+
struct FailedToConstructDocumentURIFromStringError: Error, CustomStringConvertible {
16+
let string: String
17+
18+
var description: String {
19+
return "Failed to construct DocumentURI from '\(string)'"
20+
}
21+
}
22+
1523
public struct DocumentURI: Codable, Hashable, Sendable {
1624
/// The URL that store the URIs value
1725
private let storage: URL
@@ -55,9 +63,9 @@ public struct DocumentURI: Codable, Hashable, Sendable {
5563

5664
/// Construct a DocumentURI from the given URI string, automatically parsing
5765
/// it either as a URL or an opaque URI.
58-
public init(string: String) {
66+
public init(string: String) throws {
5967
guard let url = URL(string: string) else {
60-
fatalError("Failed to construct DocumentURI from '\(string)'")
68+
throw FailedToConstructDocumentURIFromStringError(string: string)
6169
}
6270
self.init(url)
6371
}
@@ -68,7 +76,7 @@ public struct DocumentURI: Codable, Hashable, Sendable {
6876
}
6977

7078
public init(from decoder: Decoder) throws {
71-
self.init(string: try decoder.singleValueContainer().decode(String.self))
79+
try self.init(string: decoder.singleValueContainer().decode(String.self))
7280
}
7381

7482
/// Equality check to handle escape sequences in file URLs.

Sources/LanguageServerProtocol/SupportTypes/TextDocumentIdentifier.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ extension TextDocumentIdentifier: LSPAnyCodable {
2626
guard case .string(let uriString)? = dictionary[CodingKeys.uri.stringValue] else {
2727
return nil
2828
}
29-
self.uri = DocumentURI(string: uriString)
29+
guard let uri = try? DocumentURI(string: uriString) else {
30+
return nil
31+
}
32+
self.uri = uri
3033
}
3134

3235
public func encodeToLSPAny() -> LSPAny {

Sources/LanguageServerProtocol/SupportTypes/WorkspaceEdit.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ extension WorkspaceEdit: Codable {
4949
if let changesDict = try container.decodeIfPresent([String: [TextEdit]].self, forKey: .changes) {
5050
var changes = [DocumentURI: [TextEdit]]()
5151
for change in changesDict {
52-
let uri = DocumentURI(string: change.key)
52+
let uri = try DocumentURI(string: change.key)
5353
changes[uri] = change.value
5454
}
5555
self.changes = changes
@@ -311,8 +311,10 @@ extension WorkspaceEdit: LSPAnyCodable {
311311
}
312312
var dictionary = [DocumentURI: [TextEdit]]()
313313
for (key, value) in lspDict {
314-
let uri = DocumentURI(string: key)
315-
guard let edits = [TextEdit](fromLSPArray: value) else {
314+
guard
315+
let uri = try? DocumentURI(string: key),
316+
let edits = [TextEdit](fromLSPArray: value)
317+
else {
316318
return nil
317319
}
318320
dictionary[uri] = edits

Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,13 @@ public final class JSONRPCConnection: Connection {
230230
}
231231
}
232232

233-
/// Send a notification to the client that informs the user about a message decoding error and tells them to file an
234-
/// issue.
233+
/// Send a notification to the client that informs the user about a message encoding or decoding error and asks them
234+
/// to file an issue.
235235
///
236236
/// `message` describes what has gone wrong to the user.
237237
///
238238
/// - Important: Must be called on `queue`
239-
private func sendMessageDecodingErrorNotificationToClient(message: String) {
239+
private func sendMessageCodingErrorNotificationToClient(message: String) {
240240
dispatchPrecondition(condition: .onQueue(queue))
241241
let showMessage = ShowMessageNotification(
242242
type: .error,
@@ -302,7 +302,7 @@ public final class JSONRPCConnection: Connection {
302302
// That way the user at least knows that something is going wrong even if the client never gets a response
303303
// for the request.
304304
logger.fault("Ignoring request because we failed to decode the request and don't have a request ID")
305-
sendMessageDecodingErrorNotificationToClient(message: "sourcekit-lsp failed to decode a request")
305+
sendMessageCodingErrorNotificationToClient(message: "sourcekit-lsp failed to decode a request")
306306
return nil
307307
case .response:
308308
if let id = error.id {
@@ -338,19 +338,19 @@ public final class JSONRPCConnection: Connection {
338338
// `textDocument/didChange` will result in an out-of-sync state between the editor and sourcekit-lsp.
339339
// Warn the user about the error.
340340
logger.fault("Ignoring notification that may cause corrupted behavior")
341-
sendMessageDecodingErrorNotificationToClient(message: "sourcekit-lsp failed to decode a notification")
341+
sendMessageCodingErrorNotificationToClient(message: "sourcekit-lsp failed to decode a notification")
342342
return nil
343343
case .unknown:
344344
// We don't know what has gone wrong. This could be any level of badness. Inform the user about it.
345345
logger.fault("Ignoring unknown message")
346-
sendMessageDecodingErrorNotificationToClient(message: "sourcekit-lsp failed to decode a message")
346+
sendMessageCodingErrorNotificationToClient(message: "sourcekit-lsp failed to decode a message")
347347
return nil
348348
}
349349
} catch {
350350
// We don't know what has gone wrong. This could be any level of badness. Inform the user about it and ignore the
351351
// message.
352352
logger.fault("Ignoring unknown message")
353-
sendMessageDecodingErrorNotificationToClient(message: "sourcekit-lsp failed to decode an unknown message")
353+
sendMessageCodingErrorNotificationToClient(message: "sourcekit-lsp failed to decode an unknown message")
354354
return nil
355355
}
356356
}
@@ -391,7 +391,7 @@ public final class JSONRPCConnection: Connection {
391391
// We failed to parse the message header. There isn't really much we can do to recover because we lost our
392392
// anchor in the stream where new messages start. Crashing and letting ourselves be restarted by the client is
393393
// probably the best option.
394-
sendMessageDecodingErrorNotificationToClient(message: "Failed to find next message in connection to editor")
394+
sendMessageCodingErrorNotificationToClient(message: "Failed to find next message in connection to editor")
395395
fatalError("fatal error encountered while splitting JSON RPC messages \(error)")
396396
}
397397

@@ -481,8 +481,40 @@ public final class JSONRPCConnection: Connection {
481481
do {
482482
data = try encoder.encode(message)
483483
} catch {
484-
// FIXME: attempt recovery?
485-
fatalError("unexpected error while encoding response: \(error)")
484+
logger.fault("Failed to encode message: \(error.forLogging)")
485+
logger.fault("Malformed message: \(String(describing: message))")
486+
switch message {
487+
case .notification(_):
488+
// We want to send a notification to the editor but failed to encode it. Since dropping the notification might
489+
// result in getting out-of-sync state-wise with the editor (eg. for work progress notifications), inform the
490+
// user about it.
491+
sendMessageCodingErrorNotificationToClient(
492+
message: "sourcekit-lsp failed to encode a notification to the editor"
493+
)
494+
return
495+
case .request(_, _):
496+
// We want to send a notification to the editor but failed to encode it. We don't know the `reply` handle for
497+
// the request at this point so we can't synthesize an errorResponse for the request. This means that the
498+
// request will never receive a reply. Inform the user about it.
499+
sendMessageCodingErrorNotificationToClient(
500+
message: "sourcekit-lsp failed to encode a request to the editor"
501+
)
502+
return
503+
case .response(_, _):
504+
// The editor sent a request to sourcekit-lsp, which failed but we can't serialize the result back to the
505+
// client. This means that the request will never receive a reply. Inform the user about it and accept that
506+
// we'll never send a reply.
507+
sendMessageCodingErrorNotificationToClient(
508+
message: "sourcekit-lsp failed to encode a response to the editor"
509+
)
510+
return
511+
case .errorResponse(_, _):
512+
// Same as `.response`. Has an optional `id`, so can't share the case.
513+
sendMessageCodingErrorNotificationToClient(
514+
message: "sourcekit-lsp failed to encode an error response to the editor"
515+
)
516+
return
517+
}
486518
}
487519

488520
var dispatchData = DispatchData.empty

Sources/SourceKitLSP/DocumentManager.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ public final class DocumentManager {
9292
public enum Error: Swift.Error {
9393
case alreadyOpen(DocumentURI)
9494
case missingDocument(DocumentURI)
95+
case failedToConvertPosition
9596
}
9697

9798
let queue: DispatchQueue = DispatchQueue(label: "document-manager-queue")
@@ -159,7 +160,10 @@ public final class DocumentManager {
159160

160161
var sourceEdits: [SourceEdit] = []
161162
for edit in edits {
162-
sourceEdits.append(SourceEdit(edit: edit, lineTableBeforeEdit: document.latestLineTable))
163+
guard let sourceEdit = SourceEdit(edit: edit, lineTableBeforeEdit: document.latestLineTable) else {
164+
throw Error.failedToConvertPosition
165+
}
166+
sourceEdits.append(sourceEdit)
163167

164168
if let range = edit.range {
165169
document.latestLineTable.replace(
@@ -230,7 +234,11 @@ extension DocumentManager {
230234
}
231235

232236
fileprivate extension SourceEdit {
233-
init(edit: TextDocumentContentChangeEvent, lineTableBeforeEdit: LineTable) {
237+
/// Constructs a `SourceEdit` from the given `TextDocumentContentChangeEvent`.
238+
///
239+
/// Returns `nil` if the `TextDocumentContentChangeEvent` refers to line:column positions that don't exist in
240+
/// `LineTable`.
241+
init?(edit: TextDocumentContentChangeEvent, lineTableBeforeEdit: LineTable) {
234242
if let range = edit.range {
235243
guard
236244
let offset = lineTableBeforeEdit.utf8OffsetOf(
@@ -242,7 +250,7 @@ fileprivate extension SourceEdit {
242250
utf16Column: range.upperBound.utf16index
243251
)
244252
else {
245-
fatalError("invalid edit \(range)")
253+
return nil
246254
}
247255
self.init(
248256
range: AbsolutePosition(utf8Offset: offset)..<AbsolutePosition(utf8Offset: end),

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2104,14 +2104,12 @@ extension SourceKitLSPServer {
21042104
private nonisolated func extractCallHierarchyItemData(_ rawData: LSPAny?) -> (uri: DocumentURI, usr: String)? {
21052105
guard case let .dictionary(data) = rawData,
21062106
case let .string(uriString) = data["uri"],
2107-
case let .string(usr) = data["usr"]
2107+
case let .string(usr) = data["usr"],
2108+
let uri = orLog("DocumentURI for call hierarchy item", { try DocumentURI(string: uriString) })
21082109
else {
21092110
return nil
21102111
}
2111-
return (
2112-
uri: DocumentURI(string: uriString),
2113-
usr: usr
2114-
)
2112+
return (uri: uri, usr: usr)
21152113
}
21162114

21172115
func incomingCalls(_ req: CallHierarchyIncomingCallsRequest) async throws -> [CallHierarchyIncomingCall]? {
@@ -2289,14 +2287,12 @@ extension SourceKitLSPServer {
22892287
private nonisolated func extractTypeHierarchyItemData(_ rawData: LSPAny?) -> (uri: DocumentURI, usr: String)? {
22902288
guard case let .dictionary(data) = rawData,
22912289
case let .string(uriString) = data["uri"],
2292-
case let .string(usr) = data["usr"]
2290+
case let .string(usr) = data["usr"],
2291+
let uri = orLog("DocumentURI for type hierarchy item", { try DocumentURI(string: uriString) })
22932292
else {
22942293
return nil
22952294
}
2296-
return (
2297-
uri: DocumentURI(string: uriString),
2298-
usr: usr
2299-
)
2295+
return (uri: uri, usr: usr)
23002296
}
23012297

23022298
func supertypes(_ req: TypeHierarchySupertypesRequest) async throws -> [TypeHierarchyItem]? {

Sources/SourceKitLSP/Swift/Diagnostic.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ extension CodeAction {
110110
case (true, false):
111111
return "Insert '\(newText)'"
112112
case (true, true):
113-
preconditionFailure("FixIt makes no changes")
113+
logger.fault("Both oldText and newText of FixIt are empty")
114+
return "Fix"
114115
}
115116
}
116117
}

Sources/SourceKitLSP/Swift/SwiftLanguageService.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,12 @@ extension SwiftLanguageService {
518518
do {
519519
_ = try await self.sourcekitd.send(req, fileContents: nil)
520520
} catch {
521-
fatalError("failed to apply edit")
521+
logger.fault(
522+
"""
523+
failed to replace \(edit.range.lowerBound.utf8Offset):\(edit.range.upperBound.utf8Offset) by \
524+
'\(edit.replacement)' in sourcekitd
525+
"""
526+
)
522527
}
523528
}
524529

Sources/SourceKitLSP/Swift/SyntaxHighlightingTokenParser.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ struct SyntaxHighlightingTokenParser {
206206
}
207207
}
208208

209-
extension Range where Bound == Position {
209+
extension Range<Position> {
210210
/// Splits a potentially multi-line range to multiple single-line ranges.
211211
func splitToSingleLineRanges(in snapshot: DocumentSnapshot) -> [Self] {
212212
if isEmpty {
@@ -220,7 +220,8 @@ extension Range where Bound == Position {
220220
guard let startIndex = snapshot.index(of: lowerBound),
221221
let endIndex = snapshot.index(of: upperBound)
222222
else {
223-
fatalError("Range \(self) reaches outside of the document")
223+
logger.fault("Range \(self) reaches outside of the document")
224+
return [self]
224225
}
225226

226227
let text = snapshot.text[startIndex..<endIndex]

0 commit comments

Comments
 (0)