Skip to content

Commit f2df547

Browse files
committed
Change the build system to only notify delegate about changed files, not about new build settings
This defines away an entire class of data races if delegate callbacks are delivered out-of-order. If we aren’t providing the new build settings in the delegate callback, then it doesn’t matter if two `fileBuildSettingsChanged` calls change order since they don’t carry any state.
1 parent f759651 commit f2df547

13 files changed

+61
-103
lines changed

Sources/SKCore/BuildServerBuildSystem.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,7 @@ public actor BuildServerBuildSystem: MessageHandler {
202202
/// about the changed build settings.
203203
private func buildSettingsChanged(for document: DocumentURI, settings: FileBuildSettings?) async {
204204
buildSettings[document] = settings
205-
if let settings {
206-
await self.delegate?.fileBuildSettingsChanged([document: .modified(settings)])
207-
} else {
208-
await self.delegate?.fileBuildSettingsChanged([document: .removedOrUnavailable])
209-
}
205+
await self.delegate?.fileBuildSettingsChanged([document])
210206
}
211207
}
212208

Sources/SKCore/BuildSystemDelegate.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,7 @@ public protocol BuildSystemDelegate: AnyObject {
2121
func buildTargetsChanged(_ changes: [BuildTargetEvent]) async
2222

2323
/// Notify the delegate that the given files' build settings have changed.
24-
///
25-
/// The delegate should cache the new build settings for any of the given
26-
/// files that they are interested in.
27-
func fileBuildSettingsChanged(
28-
_ changedFiles: [DocumentURI: FileBuildSettingsChange]) async
24+
func fileBuildSettingsChanged(_ changedFiles: Set<DocumentURI>) async
2925

3026
/// Notify the delegate that the dependencies of the given files have changed
3127
/// and that ASTs may need to be refreshed. If the given set is empty, assume

Sources/SKCore/BuildSystemManager.swift

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,8 @@ extension BuildSystemManager {
203203

204204
let newStatus = await self.cachedStatusOrRegisterForSettings(for: mainFile, language: language)
205205

206-
if let mainChange = newStatus.buildSettingsChange,
207-
let delegate = self._delegate {
208-
let change = self.convert(change: mainChange, ofMainFile: mainFile, to: uri)
209-
await delegate.fileBuildSettingsChanged([uri: change])
206+
if newStatus.buildSettingsChange != nil, let delegate = self._delegate {
207+
await delegate.fileBuildSettingsChanged([uri])
210208
}
211209
}
212210

@@ -287,7 +285,7 @@ extension BuildSystemManager {
287285
/// Update and notify our delegate for the given main file changes if they are
288286
/// convertible into `FileBuildSettingsChange`.
289287
func updateAndNotifyStatuses(changes: [DocumentURI: MainFileStatus]) async {
290-
var changedWatchedFiles = [DocumentURI: FileBuildSettingsChange]()
288+
var changedWatchedFiles = Set<DocumentURI>()
291289
for (mainFile, status) in changes {
292290
let watches = self.watchedFiles.filter { $1.mainFile == mainFile }
293291
guard !watches.isEmpty else {
@@ -303,12 +301,11 @@ extension BuildSystemManager {
303301
guard prevStatus == .waiting || status.buildSettings != prevStatus?.buildSettings else {
304302
continue
305303
}
306-
if let change = status.buildSettingsChange {
307-
for watch in watches {
308-
let newChange =
309-
self.convert(change: change, ofMainFile: mainFile, to: watch.key)
310-
changedWatchedFiles[watch.key] = newChange
311-
}
304+
guard status.buildSettingsChange != nil else {
305+
continue
306+
}
307+
for watch in watches {
308+
changedWatchedFiles.insert(watch.key)
312309
}
313310
}
314311

@@ -367,26 +364,26 @@ extension BuildSystemManager {
367364

368365
extension BuildSystemManager: BuildSystemDelegate {
369366
// FIXME: (async) Make this method isolated once `BuildSystemDelegate` has ben asyncified
370-
public nonisolated func fileBuildSettingsChanged(_ changes: [DocumentURI: FileBuildSettingsChange]) {
367+
public nonisolated func fileBuildSettingsChanged(_ changes: Set<DocumentURI>) {
371368
Task {
372369
await fileBuildSettingsChangedImpl(changes)
373370
}
374371
}
375372

376-
public func fileBuildSettingsChangedImpl(_ changes: [DocumentURI: FileBuildSettingsChange]) async {
377-
let statusChanges: [DocumentURI: MainFileStatus] =
378-
changes.reduce(into: [:]) { (result, entry) in
379-
let mainFile = entry.key
380-
let settingsChange = entry.value
373+
public func fileBuildSettingsChangedImpl(_ changes: Set<DocumentURI>) async {
374+
var statusChanges: [DocumentURI: MainFileStatus] = [:]
375+
for mainFile in changes {
381376
let watches = self.watchedFiles.filter { $1.mainFile == mainFile }
382377
guard let firstWatch = watches.first else {
383378
// Possible notification after the file was unregistered. Ignore.
384-
return
379+
continue
385380
}
386381
let newStatus: MainFileStatus
387382

388-
if let newSettings = settingsChange.newSettings {
389-
newStatus = settingsChange.isFallback ? .fallback(newSettings) : .primary(newSettings)
383+
let settings = await self.buildSettings(for: mainFile, language: firstWatch.value.language)
384+
385+
if let settings {
386+
newStatus = settings.isFallback ? .fallback(settings.buildSettings) : .primary(settings.buildSettings)
390387
} else if let fallback = self.fallbackBuildSystem {
391388
// FIXME: we need to stop threading the language everywhere, or we need the build system
392389
// itself to pass it in here. Or alternatively cache the fallback settings/language earlier?
@@ -399,7 +396,7 @@ extension BuildSystemManager: BuildSystemDelegate {
399396
} else {
400397
newStatus = .unsupported
401398
}
402-
result[mainFile] = newStatus
399+
statusChanges[mainFile] = newStatus
403400
}
404401
await self.updateAndNotifyStatuses(changes: statusChanges)
405402
}
@@ -467,7 +464,7 @@ extension BuildSystemManager: MainFilesDelegate {
467464
public func mainFilesChangedImpl() async {
468465
let origWatched = self.watchedFiles
469466
self.watchedFiles = [:]
470-
var buildSettingsChanges = [DocumentURI: FileBuildSettingsChange]()
467+
var buildSettingsChanges = Set<DocumentURI>()
471468

472469
for (uri, state) in origWatched {
473470
let mainFiles = self._mainFilesProvider?.mainFilesContainingFile(uri) ?? []
@@ -482,9 +479,8 @@ extension BuildSystemManager: MainFilesDelegate {
482479

483480
let newStatus = await self.cachedStatusOrRegisterForSettings(
484481
for: newMainFile, language: language)
485-
if let change = newStatus.buildSettingsChange {
486-
let newChange = self.convert(change: change, ofMainFile: newMainFile, to: uri)
487-
buildSettingsChanges[uri] = newChange
482+
if newStatus.buildSettingsChange != nil {
483+
buildSettingsChanges.insert(uri)
488484
}
489485
}
490486
}

Sources/SKCore/CompilationDatabaseBuildSystem.swift

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,7 @@ extension CompilationDatabaseBuildSystem: BuildSystem {
9696

9797
guard let delegate = self.delegate else { return }
9898

99-
let settings = self.settings(for: uri)
100-
await delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)])
99+
await delegate.fileBuildSettingsChanged([uri])
101100
}
102101

103102
/// We don't support change watching.
@@ -148,13 +147,9 @@ extension CompilationDatabaseBuildSystem: BuildSystem {
148147
self.compdb = tryLoadCompilationDatabase(directory: projectRoot, self.fileSystem)
149148

150149
if let delegate = self.delegate {
151-
var changedFiles: [DocumentURI: FileBuildSettingsChange] = [:]
150+
var changedFiles = Set<DocumentURI>()
152151
for (uri, _) in self.watchedFiles {
153-
if let settings = self.settings(for: uri) {
154-
changedFiles[uri] = FileBuildSettingsChange(settings)
155-
} else {
156-
changedFiles[uri] = .removedOrUnavailable
157-
}
152+
changedFiles.insert(uri)
158153
}
159154
await delegate.fileBuildSettingsChanged(changedFiles)
160155
}

Sources/SKCore/FallbackBuildSystem.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,8 @@ public final class FallbackBuildSystem: BuildSystem {
6262
public func registerForChangeNotifications(for uri: DocumentURI, language: Language) {
6363
guard let delegate = self.delegate else { return }
6464

65-
let settings = self.buildSettings(for: uri, language: language)
6665
Task {
67-
await delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)])
66+
await delegate.fileBuildSettingsChanged([uri])
6867
}
6968
}
7069

Sources/SKSwiftPMWorkspace/SwiftPMWorkspace.swift

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -242,15 +242,9 @@ extension SwiftPMWorkspace {
242242
})
243243

244244
guard let delegate = self.delegate else { return }
245-
var changedFiles: [DocumentURI: FileBuildSettingsChange] = [:]
246-
for (uri, language) in self.watchedFiles {
247-
orLog {
248-
if let settings = try self.buildSettings(for: uri, language: language) {
249-
changedFiles[uri] = FileBuildSettingsChange(settings)
250-
} else {
251-
changedFiles[uri] = .removedOrUnavailable
252-
}
253-
}
245+
var changedFiles = Set<DocumentURI>()
246+
for (uri, _) in self.watchedFiles {
247+
changedFiles.insert(uri)
254248
}
255249
await delegate.fileBuildSettingsChanged(changedFiles)
256250
await delegate.fileHandlingCapabilityChanged()
@@ -316,11 +310,7 @@ extension SwiftPMWorkspace: SKCore.BuildSystem {
316310
} catch {
317311
log("error computing settings: \(error)")
318312
}
319-
if let settings = settings {
320-
await delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)])
321-
} else {
322-
await delegate.fileBuildSettingsChanged([uri: .removedOrUnavailable])
323-
}
313+
await delegate.fileBuildSettingsChanged([uri])
324314
}
325315

326316
/// Unregister the given file for build-system level change notifications, such as command

Sources/SourceKitLSP/Clang/ClangLanguageServer.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ extension ClangLanguageServerShim {
404404
// Send clangd the build settings for the new file. We need to do this before
405405
// sending the open notification, so that the initial diagnostics already
406406
// have build settings.
407-
await documentUpdatedBuildSettings(note.textDocument.uri, change: .removedOrUnavailable)
407+
await documentUpdatedBuildSettings(note.textDocument.uri)
408408
clangd.send(note)
409409
}
410410

@@ -427,7 +427,7 @@ extension ClangLanguageServerShim {
427427

428428
// MARK: - Build System Integration
429429

430-
public func documentUpdatedBuildSettings(_ uri: DocumentURI, change: FileBuildSettingsChange) async {
430+
public func documentUpdatedBuildSettings(_ uri: DocumentURI) async {
431431
guard let url = uri.fileURL else {
432432
// FIXME: The clang workspace can probably be reworked to support non-file URIs.
433433
log("Received updated build settings for non-file URI '\(uri)'. Ignoring the update.")

Sources/SourceKitLSP/SourceKitServer.swift

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,7 @@ extension SourceKitServer: BuildSystemDelegate {
666666

667667
// FIXME: (async) Make this method isolated once `BuildSystemDelegate` has been asyncified
668668
/// Non-async variant that executes `fileBuildSettingsChangedImpl` in a new task.
669-
public nonisolated func fileBuildSettingsChanged(_ changedFiles: [DocumentURI: FileBuildSettingsChange]) {
669+
public nonisolated func fileBuildSettingsChanged(_ changedFiles: Set<DocumentURI>) {
670670
Task {
671671
await self.fileBuildSettingsChangedImpl(changedFiles)
672672
}
@@ -676,10 +676,8 @@ extension SourceKitServer: BuildSystemDelegate {
676676
/// This has two primary cases:
677677
/// - Initial settings reported for a given file, now we can fully open it
678678
/// - Changed settings for an already open file
679-
public func fileBuildSettingsChangedImpl(
680-
_ changedFiles: [DocumentURI: FileBuildSettingsChange]
681-
) async {
682-
for (uri, change) in changedFiles {
679+
public func fileBuildSettingsChangedImpl(_ changedFiles: Set<DocumentURI>) async {
680+
for uri in changedFiles {
683681
// Non-ready documents should be considered open even though we haven't
684682
// opened it with the language service yet.
685683
guard self.documentManager.openDocuments.contains(uri) else { continue }
@@ -704,7 +702,7 @@ extension SourceKitServer: BuildSystemDelegate {
704702
}
705703

706704
// Notify the language server so it can apply the proper arguments.
707-
await service.documentUpdatedBuildSettings(uri, change: change)
705+
await service.documentUpdatedBuildSettings(uri)
708706

709707
// Catch up on any queued notifications and requests.
710708
while !(documentToPendingQueue[uri]?.queue.isEmpty ?? true) {
@@ -725,7 +723,7 @@ extension SourceKitServer: BuildSystemDelegate {
725723
// Case 2: changed settings for an already open file.
726724
log("Build settings changed for opened file \(uri)")
727725
if let service = workspace.documentService[uri] {
728-
await service.documentUpdatedBuildSettings(uri, change: change)
726+
await service.documentUpdatedBuildSettings(uri)
729727
}
730728
}
731729
}

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ extension SwiftLanguageServer {
488488
self.updateSyntacticTokens(for: snapshot)
489489
}
490490

491-
public func documentUpdatedBuildSettings(_ uri: DocumentURI, change: FileBuildSettingsChange) async {
491+
public func documentUpdatedBuildSettings(_ uri: DocumentURI) async {
492492
// We may not have a snapshot if this is called just before `openDocument`.
493493
guard let snapshot = self.documentManager.latestSnapshot(uri) else {
494494
return

Sources/SourceKitLSP/ToolchainLanguageServer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public protocol ToolchainLanguageServer: AnyObject {
7272
/// Sent when the `BuildSystem` has resolved build settings, such as for the intial build settings
7373
/// or when the settings have changed (e.g. modified build system files). This may be sent before
7474
/// the respective `DocumentURI` has been opened.
75-
func documentUpdatedBuildSettings(_ uri: DocumentURI, change: FileBuildSettingsChange) async
75+
func documentUpdatedBuildSettings(_ uri: DocumentURI) async
7676

7777
/// Sent when the `BuildSystem` has detected that dependencies of the given file have changed
7878
/// (e.g. header files, swiftmodule files, other compiler input files).

Tests/SKCoreTests/BuildServerBuildSystemTests.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,8 @@ final class TestDelegate: BuildSystemDelegate {
9898
}
9999
}
100100

101-
func fileBuildSettingsChanged(
102-
_ changedFiles: [DocumentURI: FileBuildSettingsChange]) {
103-
for (uri, _) in changedFiles {
101+
func fileBuildSettingsChanged(_ changedFiles: Set<DocumentURI>) {
102+
for uri in changedFiles {
104103
settingsExpectations[uri]?.fulfill()
105104
}
106105
}

Tests/SKCoreTests/BuildSystemManagerTests.swift

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ final class BuildSystemManagerTests: XCTestCase {
110110
bs.map[a] = nil
111111
let changed = expectation(description: "changed settings")
112112
await del.setExpected([(a, .swift, nil, changed, #file, #line)])
113-
bsm.fileBuildSettingsChanged([a: .removedOrUnavailable])
113+
bsm.fileBuildSettingsChanged([a])
114114
try await fulfillmentOfOrThrow([changed])
115115
}
116116

@@ -133,7 +133,7 @@ final class BuildSystemManagerTests: XCTestCase {
133133
bs.map[a] = FileBuildSettings(compilerArguments: ["x"])
134134
let changed = expectation(description: "changed settings")
135135
await del.setExpected([(a, .swift, bs.map[a]!, changed, #file, #line)])
136-
bsm.fileBuildSettingsChanged([a: .modified(bs.map[a]!)])
136+
bsm.fileBuildSettingsChanged([a])
137137
try await fulfillmentOfOrThrow([changed])
138138
}
139139

@@ -158,13 +158,13 @@ final class BuildSystemManagerTests: XCTestCase {
158158
bs.map[a] = FileBuildSettings(compilerArguments: ["non-fallback", "args"])
159159
let changed = expectation(description: "changed settings")
160160
await del.setExpected([(a, .swift, bs.map[a]!, changed, #file, #line)])
161-
bsm.fileBuildSettingsChanged([a: .modified(bs.map[a]!)])
161+
bsm.fileBuildSettingsChanged([a])
162162
try await fulfillmentOfOrThrow([changed])
163163

164164
bs.map[a] = nil
165165
let revert = expectation(description: "revert to fallback settings")
166166
await del.setExpected([(a, .swift, fallbackSettings, revert, #file, #line)])
167-
bsm.fileBuildSettingsChanged([a: .removedOrUnavailable])
167+
bsm.fileBuildSettingsChanged([a])
168168
try await fulfillmentOfOrThrow([revert])
169169
}
170170

@@ -196,7 +196,7 @@ final class BuildSystemManagerTests: XCTestCase {
196196
bs.map[b] = FileBuildSettings(compilerArguments: ["yy"])
197197
let changed = expectation(description: "changed settings")
198198
await del.setExpected([(a, .swift, bs.map[a]!, changed, #file, #line)])
199-
bsm.fileBuildSettingsChanged([a: .modified(bs.map[a]!)])
199+
bsm.fileBuildSettingsChanged([a])
200200
try await fulfillmentOfOrThrow([changed])
201201

202202
// Test multiple changes.
@@ -208,10 +208,7 @@ final class BuildSystemManagerTests: XCTestCase {
208208
(a, .swift, bs.map[a]!, changedBothA, #file, #line),
209209
(b, .swift, bs.map[b]!, changedBothB, #file, #line),
210210
])
211-
bsm.fileBuildSettingsChanged([
212-
a:. modified(bs.map[a]!),
213-
b: .modified(bs.map[b]!)
214-
])
211+
bsm.fileBuildSettingsChanged([a, b])
215212
try await fulfillmentOfOrThrow([changedBothA, changedBothB])
216213
}
217214

@@ -245,9 +242,7 @@ final class BuildSystemManagerTests: XCTestCase {
245242
bs.map[b] = nil
246243
let changed = expectation(description: "changed settings")
247244
await del.setExpected([(b, .swift, nil, changed, #file, #line)])
248-
bsm.fileBuildSettingsChanged([
249-
b: .removedOrUnavailable
250-
])
245+
bsm.fileBuildSettingsChanged([b])
251246
try await fulfillmentOfOrThrow([changed])
252247
}
253248

@@ -354,7 +349,7 @@ final class BuildSystemManagerTests: XCTestCase {
354349
(h1, .c, newArgsH1, changed1, #file, #line),
355350
(h2, .c, newArgsH2, changed2, #file, #line),
356351
])
357-
bsm.fileBuildSettingsChanged([cpp: .modified(bs.map[cpp]!)])
352+
bsm.fileBuildSettingsChanged([cpp])
358353

359354
try await fulfillmentOfOrThrow([changed1, changed2])
360355
}
@@ -403,11 +398,7 @@ final class BuildSystemManagerTests: XCTestCase {
403398
await bsm.unregisterForChangeNotifications(for: c)
404399
// At this point only b is registered, but that can race with notifications,
405400
// so ensure nothing bad happens and we still get the notification for b.
406-
bsm.fileBuildSettingsChanged([
407-
a: .modified(bs.map[a]!),
408-
b: .modified(bs.map[b]!),
409-
c: .modified(bs.map[c]!)
410-
])
401+
bsm.fileBuildSettingsChanged([a, b, c])
411402

412403
try await fulfillmentOfOrThrow([changedB])
413404
}
@@ -484,8 +475,7 @@ class ManualBuildSystem: BuildSystem {
484475
}
485476

486477
func registerForChangeNotifications(for uri: DocumentURI, language: Language) async {
487-
let settings = self.buildSettings(for: uri, language: language)
488-
await self.delegate?.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)])
478+
await self.delegate?.fileBuildSettingsChanged([uri])
489479
}
490480

491481
func unregisterForChangeNotifications(for: DocumentURI) {
@@ -536,8 +526,8 @@ private actor BSMDelegate: BuildSystemDelegate {
536526
}()
537527
}
538528

539-
func fileBuildSettingsChanged(_ changes: [DocumentURI: FileBuildSettingsChange]) async {
540-
for (uri, _) in changes {
529+
func fileBuildSettingsChanged(_ changedFiles: Set<DocumentURI>) async {
530+
for uri in changedFiles {
541531
guard let expected = expected.first(where: { $0.uri == uri }) else {
542532
XCTFail("unexpected settings change for \(uri)")
543533
continue

0 commit comments

Comments
 (0)