Skip to content

Commit d1b527e

Browse files
committed
Make the LanguageServerProtocolJSONRPC module build with strict concurrency enabled
1 parent 259da45 commit d1b527e

File tree

8 files changed

+129
-90
lines changed

8 files changed

+129
-90
lines changed

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ let package = Package(
119119
"LanguageServerProtocol",
120120
"LSPLogging",
121121
],
122-
exclude: ["CMakeLists.txt"]
122+
exclude: ["CMakeLists.txt"],
123+
swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]
123124
),
124125

125126
.testTarget(

Sources/LSPLogging/CustomLogStringConvertible.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import Foundation
1515

1616
/// An object that can printed for logging and also offers a redacted description
1717
/// when logging in contexts in which private information shouldn't be captured.
18-
public protocol CustomLogStringConvertible: CustomStringConvertible {
18+
public protocol CustomLogStringConvertible: CustomStringConvertible, Sendable {
1919
/// A full description of the object.
2020
var description: String { get }
2121

@@ -30,7 +30,7 @@ public protocol CustomLogStringConvertible: CustomStringConvertible {
3030
/// There currently is no way to get equivalent functionality in pure Swift. We
3131
/// thus pass this object to OSLog, which just forwards to `description` or
3232
/// `redactedDescription` of an object that implements `CustomLogStringConvertible`.
33-
public class CustomLogStringConvertibleWrapper: NSObject {
33+
public final class CustomLogStringConvertibleWrapper: NSObject, Sendable {
3434
private let underlyingObject: any CustomLogStringConvertible
3535

3636
fileprivate init(_ underlyingObject: any CustomLogStringConvertible) {

Sources/LanguageServerProtocol/Connection.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ public protocol Connection: AnyObject, Sendable {
2121
/// Send a request and (asynchronously) receive a reply.
2222
func send<Request: RequestType>(
2323
_ request: Request,
24-
reply: @escaping (LSPResult<Request.Response>) -> Void
24+
reply: @escaping @Sendable (LSPResult<Request.Response>) -> Void
2525
) -> RequestID
2626
}
2727

2828
/// An abstract message handler, such as a language server or client.
29-
public protocol MessageHandler: AnyObject {
29+
public protocol MessageHandler: AnyObject, Sendable {
3030

3131
/// Handle a notification without a reply.
3232
///

Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift

Lines changed: 92 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ public final class JSONRPCConnection: Connection {
3030
/// The message handler that handles requests and notifications sent through this connection.
3131
///
3232
/// Access to this must be be guaranteed to be sequential to avoid data races. Currently, all access are
33-
/// - `init`: Can trivially only be called once
34-
/// - `start`: Is required to be call in the same sequential code region as the initializer, so
33+
/// - `init`: Reference to `JSONRPCConnection` trivially can't have escaped to other isolation domains yet.
34+
/// - `start`: Is required to be call in the same serial code region as the initializer, so
3535
/// `JSONRPCConnection` can't have escaped to other isolation domains yet.
3636
/// - `deinit`: Can also only trivially be called once.
3737
nonisolated(unsafe) private var receiveHandler: MessageHandler?
@@ -56,35 +56,54 @@ public final class JSONRPCConnection: Connection {
5656
/// Current state of the connection, used to ensure correct usage.
5757
///
5858
/// Access to this must be be guaranteed to be sequential to avoid data races. Currently, all access are
59-
/// - `init`: Can trivially only be called once
60-
/// - `start`: Is required to be called in the same sequential region as the initializer, so
59+
/// - `init`: Reference to `JSONRPCConnection` trivially can't have escaped to other isolation domains yet.
60+
/// - `start`: Is required to be called in the same serial region as the initializer, so
6161
/// `JSONRPCConnection` can't have escaped to other isolation domains yet.
6262
/// - `_close`: Synchronized on `queue`.
6363
/// - `readyToSend`: Synchronized on `queue`.
6464
/// - `deinit`: Can also only trivially be called once.
65-
nonisolated(unsafe) private var state: State
65+
private nonisolated(unsafe) var state: State
6666

6767
/// Buffer of received bytes that haven't been parsed.
68+
///
69+
/// Access to this must be be guaranteed to be sequential to avoid data races. Currently, all access are
70+
/// - The `receiveIO` handler: This is synchronized on `queue`.
71+
/// - `requestBufferIsEmpty`: Also synchronized on `queue`.
72+
private nonisolated(unsafe) var requestBuffer: [UInt8] = []
73+
6874
@_spi(Testing)
69-
public var _requestBuffer: [UInt8] = []
75+
public var requestBufferIsEmpty: Bool {
76+
queue.sync {
77+
requestBuffer.isEmpty
78+
}
79+
}
7080

71-
private var _nextRequestID: Int = 0
81+
/// An integer that hasn't been used for a request ID yet.
82+
///
83+
/// Access to this must be be guaranteed to be sequential to avoid data races. Currently, all access are
84+
/// - `nextRequestID()`: This is synchronized on `queue`.
85+
private nonisolated(unsafe) var nextRequestIDStorage: Int = 0
7286

73-
struct OutstandingRequest {
87+
struct OutstandingRequest: Sendable {
7488
var responseType: ResponseType.Type
75-
var replyHandler: (LSPResult<Any>) -> Void
89+
var replyHandler: @Sendable (LSPResult<Any>) -> Void
7690
}
7791

78-
/// The set of currently outstanding outgoing requests along with information about how to decode and handle their responses.
79-
private var outstandingRequests: [RequestID: OutstandingRequest] = [:]
92+
/// The set of currently outstanding outgoing requests along with information about how to decode and handle their
93+
/// responses.
94+
///
95+
/// All accesses to `outstandingRequests` must be on `queue` to avoid race conditions.
96+
private nonisolated(unsafe) var outstandingRequests: [RequestID: OutstandingRequest] = [:]
8097

8198
/// A handler that will be called asynchronously when the connection is being
8299
/// closed.
83-
private var closeHandler: (() async -> Void)? = nil
100+
///
101+
/// There are no race conditions to `closeHandler` because it is only set from `start`, which is required to be called
102+
/// in the same serial code region domain as the initializer, so it's serial and the `JSONRPCConnection` can't
103+
/// have escaped to other isolation domains yet.
104+
private nonisolated(unsafe) var closeHandler: (@Sendable () async -> Void)? = nil
84105

85-
/// - Important: `start` must be called within the same sequential code region that created the `JSONRPCConnection`.
86-
/// This means that no `await` calls must exist between the creation of the connection and the call to `start` and
87-
/// the `JSONRPCConnection` must not be accessible to any other threads yet.
106+
/// - Important: `start` must be called before sending any data over the `JSONRPCConnection`.
88107
public init(
89108
name: String,
90109
protocol messageRegistry: MessageRegistry,
@@ -95,7 +114,8 @@ public final class JSONRPCConnection: Connection {
95114
self.name = name
96115
self.receiveHandler = nil
97116
#if os(Linux) || os(Android)
98-
// We receive a `SIGPIPE` if we write to a pipe that points to a crashed process. This in particular happens if the target of a `JSONRPCConnection` has crashed and we try to send it a message.
117+
// We receive a `SIGPIPE` if we write to a pipe that points to a crashed process. This in particular happens if the
118+
// target of a `JSONRPCConnection` has crashed and we try to send it a message.
99119
// On Darwin, `DispatchIO` ignores `SIGPIPE` for the pipes handled by it, but that features is not available on Linux.
100120
// Instead, globally ignore `SIGPIPE` on Linux to prevent us from crashing if the `JSONRPCConnection`'s target crashes.
101121
globallyDisableSigpipe()
@@ -168,49 +188,49 @@ public final class JSONRPCConnection: Connection {
168188
///
169189
/// - parameter receiveHandler: The message handler to invoke for requests received on the `inFD`.
170190
///
171-
/// - Important: `start` must be called within the same sequential code region that created the `JSONRPCConnection`.
172-
/// This means that no `await` calls must exist between the creation of the connection and the call to `start` and
173-
/// the `JSONRPCConnection` must not be accessible to any other threads yet.
174-
public func start(receiveHandler: MessageHandler, closeHandler: @escaping () async -> Void = {}) {
175-
precondition(state == .created)
176-
state = .running
177-
self.receiveHandler = receiveHandler
178-
self.closeHandler = closeHandler
179-
180-
receiveIO.read(offset: 0, length: Int.max, queue: queue) { done, data, errorCode in
181-
guard errorCode == 0 else {
182-
#if !os(Windows)
183-
if errorCode != POSIXError.ECANCELED.rawValue {
184-
logger.error("IO error reading \(errorCode)")
191+
/// - Important: `start` must be called before sending any data over the `JSONRPCConnection`.
192+
public func start(receiveHandler: MessageHandler, closeHandler: @escaping @Sendable () async -> Void = {}) {
193+
queue.sync {
194+
precondition(state == .created)
195+
state = .running
196+
self.receiveHandler = receiveHandler
197+
self.closeHandler = closeHandler
198+
199+
receiveIO.read(offset: 0, length: Int.max, queue: queue) { done, data, errorCode in
200+
guard errorCode == 0 else {
201+
#if !os(Windows)
202+
if errorCode != POSIXError.ECANCELED.rawValue {
203+
logger.error("IO error reading \(errorCode)")
204+
}
205+
#endif
206+
if done { self.closeAssumingOnQueue() }
207+
return
185208
}
186-
#endif
187-
if done { self.closeOnQueue() }
188-
return
189-
}
190-
191-
if done {
192-
self.closeOnQueue()
193-
return
194-
}
195209

196-
guard let data = data, !data.isEmpty else {
197-
return
198-
}
210+
if done {
211+
self.closeAssumingOnQueue()
212+
return
213+
}
199214

200-
// Parse and handle any messages in `buffer + data`, leaving any remaining unparsed bytes in `buffer`.
201-
if self._requestBuffer.isEmpty {
202-
data.withUnsafeBytes { (pointer: UnsafePointer<UInt8>) in
203-
let rest = self.parseAndHandleMessages(from: UnsafeBufferPointer(start: pointer, count: data.count))
204-
self._requestBuffer.append(contentsOf: rest)
215+
guard let data = data, !data.isEmpty else {
216+
return
205217
}
206-
} else {
207-
self._requestBuffer.append(contentsOf: data)
208-
var unused = 0
209-
self._requestBuffer.withUnsafeBufferPointer { buffer in
210-
let rest = self.parseAndHandleMessages(from: buffer)
211-
unused = rest.count
218+
219+
// Parse and handle any messages in `buffer + data`, leaving any remaining unparsed bytes in `buffer`.
220+
if self.requestBuffer.isEmpty {
221+
data.withUnsafeBytes { (pointer: UnsafePointer<UInt8>) in
222+
let rest = self.parseAndHandleMessages(from: UnsafeBufferPointer(start: pointer, count: data.count))
223+
self.requestBuffer.append(contentsOf: rest)
224+
}
225+
} else {
226+
self.requestBuffer.append(contentsOf: data)
227+
var unused = 0
228+
self.requestBuffer.withUnsafeBufferPointer { buffer in
229+
let rest = self.parseAndHandleMessages(from: buffer)
230+
unused = rest.count
231+
}
232+
self.requestBuffer.removeFirst(self.requestBuffer.count - unused)
212233
}
213-
self._requestBuffer.removeFirst(self._requestBuffer.count - unused)
214234
}
215235
}
216236
}
@@ -231,7 +251,10 @@ public final class JSONRPCConnection: Connection {
231251
}
232252

233253
/// Parse and handle all messages in `bytes`, returning a slice containing any remaining incomplete data.
254+
///
255+
/// - Important: Must be called on `queue`
234256
func parseAndHandleMessages(from bytes: UnsafeBufferPointer<UInt8>) -> UnsafeBufferPointer<UInt8>.SubSequence {
257+
dispatchPrecondition(condition: .onQueue(queue))
235258
let decoder = JSONDecoder()
236259

237260
// Set message registry to use for model decoding.
@@ -299,7 +322,10 @@ public final class JSONRPCConnection: Connection {
299322
}
300323

301324
/// Handle a single message by dispatching it to `receiveHandler` or an appropriate reply handler.
325+
///
326+
/// - Important: Must be called on `queue`
302327
func handle(_ message: JSONRPCMessage) {
328+
dispatchPrecondition(condition: .onQueue(queue))
303329
switch message {
304330
case .notification(let notification):
305331
notification._handle(self.receiveHandler!)
@@ -341,11 +367,11 @@ public final class JSONRPCConnection: Connection {
341367
sendIO.write(offset: 0, data: dispatchData, queue: sendQueue) { [weak self] done, _, errorCode in
342368
if errorCode != 0 {
343369
logger.error("IO error sending message \(errorCode)")
344-
if done {
370+
if done, let self {
345371
// An unrecoverable error occurs on the channel’s file descriptor.
346372
// Close the connection.
347-
self?.queue.async {
348-
self?.closeOnQueue()
373+
self.queue.async {
374+
self.closeAssumingOnQueue()
349375
}
350376
}
351377
}
@@ -398,13 +424,13 @@ public final class JSONRPCConnection: Connection {
398424
/// The user-provided close handler will be called *asynchronously* when all outstanding I/O
399425
/// operations have completed. No new I/O will be accepted after `close` returns.
400426
public func close() {
401-
queue.sync { closeOnQueue() }
427+
queue.sync { closeAssumingOnQueue() }
402428
}
403429

404430
/// Close the connection, assuming that the code is already executing on `queue`.
405431
///
406432
/// - Important: Must be called on `queue`.
407-
func closeOnQueue() {
433+
private func closeAssumingOnQueue() {
408434
dispatchPrecondition(condition: .onQueue(queue))
409435
sendQueue.sync {
410436
guard state == .running else { return }
@@ -419,9 +445,12 @@ public final class JSONRPCConnection: Connection {
419445
}
420446

421447
/// Request id for the next outgoing request.
422-
func nextRequestID() -> RequestID {
423-
_nextRequestID += 1
424-
return .number(_nextRequestID)
448+
///
449+
/// - Important: Must be called on `queue`
450+
private func nextRequestID() -> RequestID {
451+
dispatchPrecondition(condition: .onQueue(queue))
452+
nextRequestIDStorage += 1
453+
return .number(nextRequestIDStorage)
425454
}
426455

427456
// MARK: Connection interface
@@ -444,7 +473,7 @@ public final class JSONRPCConnection: Connection {
444473
/// When the receiving end replies to the request, execute `reply` with the response.
445474
public func send<Request: RequestType>(
446475
_ request: Request,
447-
reply: @escaping (LSPResult<Request.Response>) -> Void
476+
reply: @escaping @Sendable (LSPResult<Request.Response>) -> Void
448477
) -> RequestID {
449478
let id: RequestID = self.queue.sync {
450479
let id = nextRequestID()

Sources/SourceKitLSP/CapabilityRegistry.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ public final actor CapabilityRegistry {
196196
public func registerDidChangeWatchedFiles(
197197
watchers: [FileSystemWatcher],
198198
server: SourceKitServer
199-
) {
199+
) async {
200200
guard clientHasDynamicDidChangeWatchedFilesRegistration else { return }
201201
if let registration = didChangeWatchedFiles {
202202
if watchers != registration.watchers {
@@ -216,11 +216,11 @@ public final actor CapabilityRegistry {
216216

217217
self.didChangeWatchedFiles = registrationOptions
218218

219-
let _ = server.client.send(RegisterCapabilityRequest(registrations: [registration])) { result in
220-
if let error = result.failure {
221-
logger.error("Failed to dynamically register for watched files: \(error.forLogging)")
222-
self.didChangeWatchedFiles = nil
223-
}
219+
do {
220+
_ = try await server.client.send(RegisterCapabilityRequest(registrations: [registration]))
221+
} catch {
222+
logger.error("Failed to dynamically register for watched files: \(error.forLogging)")
223+
self.didChangeWatchedFiles = nil
224224
}
225225
}
226226

Sources/SourceKitLSP/Clang/ClangLanguageServer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ extension ClangLanguageServerShim {
445445
await withCheckedContinuation { continuation in
446446
_ = clangd.send(ShutdownRequest()) { _ in
447447
Task {
448-
self.clangd.send(ExitNotification())
448+
await self.clangd.send(ExitNotification())
449449
continuation.resume()
450450
}
451451
}

Sources/SourceKitLSP/SourceKitServer.swift

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -138,24 +138,33 @@ final actor WorkDoneProgressState {
138138
state = .creating
139139
// Discard the handle. We don't support cancellation of the creation of a work done progress.
140140
_ = server.client.send(CreateWorkDoneProgressRequest(token: token)) { result in
141-
if result.success != nil {
142-
if self.activeTasks == 0 {
143-
// ActiveTasks might have been decreased while we created the `WorkDoneProgress`
144-
self.state = .noProgress
145-
server.client.send(WorkDoneProgress(token: self.token, value: .end(WorkDoneProgressEnd())))
146-
} else {
147-
self.state = .created
148-
server.client.send(
149-
WorkDoneProgress(token: self.token, value: .begin(WorkDoneProgressBegin(title: self.title)))
150-
)
151-
}
152-
} else {
153-
self.state = .progressCreationFailed
141+
Task {
142+
await self.handleCreateWorkDoneProgressResponse(result, server: server)
154143
}
155144
}
156145
}
157146
}
158147

148+
private func handleCreateWorkDoneProgressResponse(
149+
_ result: Result<VoidResponse, ResponseError>,
150+
server: SourceKitServer
151+
) {
152+
if result.success != nil {
153+
if self.activeTasks == 0 {
154+
// ActiveTasks might have been decreased while we created the `WorkDoneProgress`
155+
self.state = .noProgress
156+
server.client.send(WorkDoneProgress(token: self.token, value: .end(WorkDoneProgressEnd())))
157+
} else {
158+
self.state = .created
159+
server.client.send(
160+
WorkDoneProgress(token: self.token, value: .begin(WorkDoneProgressBegin(title: self.title)))
161+
)
162+
}
163+
} else {
164+
self.state = .progressCreationFailed
165+
}
166+
}
167+
159168
/// End a new task stated using `startProgress`.
160169
///
161170
/// If this drops the active task count to 0, the work done progress is ended on the client.

Tests/LanguageServerProtocolJSONRPCTests/ConnectionTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,9 @@ class ConnectionTests: XCTestCase {
9090

9191
try await fulfillmentOfOrThrow([expectation2])
9292

93-
// Close the connection before accessing _requestBuffer, which ensures we don't race.
93+
// Close the connection before accessing requestBuffer, which ensures we don't race.
9494
connection.serverToClientConnection.close()
95-
XCTAssertEqual(connection.serverToClientConnection._requestBuffer, [])
95+
XCTAssert(connection.serverToClientConnection.requestBufferIsEmpty)
9696
}
9797

9898
func testEchoError() {

0 commit comments

Comments
 (0)