Skip to content

Commit dffcc93

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 93dfc3d commit dffcc93

13 files changed

+59
-104
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
@@ -210,10 +210,8 @@ extension BuildSystemManager {
210210

211211
let newStatus = await self.cachedStatusOrRegisterForSettings(for: mainFile, language: language)
212212

213-
if let mainChange = newStatus.buildSettingsChange,
214-
let delegate = self._delegate {
215-
let change = self.convert(change: mainChange, ofMainFile: mainFile, to: uri)
216-
await delegate.fileBuildSettingsChanged([uri: change])
213+
if newStatus.buildSettingsChange != nil, let delegate = self._delegate {
214+
await delegate.fileBuildSettingsChanged([uri])
217215
}
218216
}
219217

@@ -294,7 +292,7 @@ extension BuildSystemManager {
294292
/// Update and notify our delegate for the given main file changes if they are
295293
/// convertible into `FileBuildSettingsChange`.
296294
func updateAndNotifyStatuses(changes: [DocumentURI: MainFileStatus]) async {
297-
var changedWatchedFiles = [DocumentURI: FileBuildSettingsChange]()
295+
var changedWatchedFiles = Set<DocumentURI>()
298296
for (mainFile, status) in changes {
299297
let watches = self.watchedFiles.filter { $1.mainFile == mainFile }
300298
guard !watches.isEmpty else {
@@ -310,12 +308,11 @@ extension BuildSystemManager {
310308
guard prevStatus == .waiting || status.buildSettings != prevStatus?.buildSettings else {
311309
continue
312310
}
313-
if let change = status.buildSettingsChange {
314-
for watch in watches {
315-
let newChange =
316-
self.convert(change: change, ofMainFile: mainFile, to: watch.key)
317-
changedWatchedFiles[watch.key] = newChange
318-
}
311+
guard status.buildSettingsChange != nil else {
312+
continue
313+
}
314+
for watch in watches {
315+
changedWatchedFiles.insert(watch.key)
319316
}
320317
}
321318

@@ -374,26 +371,26 @@ extension BuildSystemManager {
374371

375372
extension BuildSystemManager: BuildSystemDelegate {
376373
// FIXME: (async) Make this method isolated once `BuildSystemDelegate` has ben asyncified
377-
public nonisolated func fileBuildSettingsChanged(_ changes: [DocumentURI: FileBuildSettingsChange]) {
374+
public nonisolated func fileBuildSettingsChanged(_ changes: Set<DocumentURI>) {
378375
Task {
379376
await fileBuildSettingsChangedImpl(changes)
380377
}
381378
}
382379

383-
public func fileBuildSettingsChangedImpl(_ changes: [DocumentURI: FileBuildSettingsChange]) async {
384-
let statusChanges: [DocumentURI: MainFileStatus] =
385-
changes.reduce(into: [:]) { (result, entry) in
386-
let mainFile = entry.key
387-
let settingsChange = entry.value
380+
public func fileBuildSettingsChangedImpl(_ changes: Set<DocumentURI>) async {
381+
var statusChanges: [DocumentURI: MainFileStatus] = [:]
382+
for mainFile in changes {
388383
let watches = self.watchedFiles.filter { $1.mainFile == mainFile }
389384
guard let firstWatch = watches.first else {
390385
// Possible notification after the file was unregistered. Ignore.
391-
return
386+
continue
392387
}
393388
let newStatus: MainFileStatus
394389

395-
if let newSettings = settingsChange.newSettings {
396-
newStatus = settingsChange.isFallback ? .fallback(newSettings) : .primary(newSettings)
390+
let settings = await self.buildSettings(for: mainFile, language: firstWatch.value.language)
391+
392+
if let settings {
393+
newStatus = settings.isFallback ? .fallback(settings.buildSettings) : .primary(settings.buildSettings)
397394
} else if let fallback = self.fallbackBuildSystem {
398395
// FIXME: we need to stop threading the language everywhere, or we need the build system
399396
// itself to pass it in here. Or alternatively cache the fallback settings/language earlier?
@@ -406,7 +403,7 @@ extension BuildSystemManager: BuildSystemDelegate {
406403
} else {
407404
newStatus = .unsupported
408405
}
409-
result[mainFile] = newStatus
406+
statusChanges[mainFile] = newStatus
410407
}
411408
await self.updateAndNotifyStatuses(changes: statusChanges)
412409
}
@@ -474,7 +471,7 @@ extension BuildSystemManager: MainFilesDelegate {
474471
public func mainFilesChangedImpl() async {
475472
let origWatched = self.watchedFiles
476473
self.watchedFiles = [:]
477-
var buildSettingsChanges = [DocumentURI: FileBuildSettingsChange]()
474+
var buildSettingsChanges = Set<DocumentURI>()
478475

479476
for (uri, state) in origWatched {
480477
let mainFiles = self._mainFilesProvider?.mainFilesContainingFile(uri) ?? []
@@ -489,9 +486,8 @@ extension BuildSystemManager: MainFilesDelegate {
489486

490487
let newStatus = await self.cachedStatusOrRegisterForSettings(
491488
for: newMainFile, language: language)
492-
if let change = newStatus.buildSettingsChange {
493-
let newChange = self.convert(change: change, ofMainFile: newMainFile, to: uri)
494-
buildSettingsChanges[uri] = newChange
489+
if newStatus.buildSettingsChange != nil {
490+
buildSettingsChanges.insert(uri)
495491
}
496492
}
497493
}

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: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -242,16 +242,7 @@ 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-
}
254-
}
245+
let changedFiles = Set<DocumentURI>(self.watchedFiles.keys)
255246
await delegate.fileBuildSettingsChanged(changedFiles)
256247
await delegate.fileHandlingCapabilityChanged()
257248
}
@@ -316,11 +307,7 @@ extension SwiftPMWorkspace: SKCore.BuildSystem {
316307
} catch {
317308
log("error computing settings: \(error)")
318309
}
319-
if let settings = settings {
320-
await delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)])
321-
} else {
322-
await delegate.fileBuildSettingsChanged([uri: .removedOrUnavailable])
323-
}
310+
await delegate.fileBuildSettingsChanged([uri])
324311
}
325312

326313
/// 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
@@ -649,7 +649,7 @@ extension SourceKitServer: BuildSystemDelegate {
649649

650650
// FIXME: (async) Make this method isolated once `BuildSystemDelegate` has been asyncified
651651
/// Non-async variant that executes `fileBuildSettingsChangedImpl` in a new task.
652-
public nonisolated func fileBuildSettingsChanged(_ changedFiles: [DocumentURI: FileBuildSettingsChange]) {
652+
public nonisolated func fileBuildSettingsChanged(_ changedFiles: Set<DocumentURI>) {
653653
Task {
654654
await self.fileBuildSettingsChangedImpl(changedFiles)
655655
}
@@ -659,10 +659,8 @@ extension SourceKitServer: BuildSystemDelegate {
659659
/// This has two primary cases:
660660
/// - Initial settings reported for a given file, now we can fully open it
661661
/// - Changed settings for an already open file
662-
public func fileBuildSettingsChangedImpl(
663-
_ changedFiles: [DocumentURI: FileBuildSettingsChange]
664-
) async {
665-
for (uri, change) in changedFiles {
662+
public func fileBuildSettingsChangedImpl(_ changedFiles: Set<DocumentURI>) async {
663+
for uri in changedFiles {
666664
// Non-ready documents should be considered open even though we haven't
667665
// opened it with the language service yet.
668666
guard self.documentManager.openDocuments.contains(uri) else { continue }
@@ -687,7 +685,7 @@ extension SourceKitServer: BuildSystemDelegate {
687685
}
688686

689687
// Notify the language server so it can apply the proper arguments.
690-
await service.documentUpdatedBuildSettings(uri, change: change)
688+
await service.documentUpdatedBuildSettings(uri)
691689

692690
// Catch up on any queued notifications and requests.
693691
while !(documentToPendingQueue[uri]?.queue.isEmpty ?? true) {
@@ -708,7 +706,7 @@ extension SourceKitServer: BuildSystemDelegate {
708706
// Case 2: changed settings for an already open file.
709707
log("Build settings changed for opened file \(uri)")
710708
if let service = workspace.documentService[uri] {
711-
await service.documentUpdatedBuildSettings(uri, change: change)
709+
await service.documentUpdatedBuildSettings(uri)
712710
}
713711
}
714712
}

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,7 @@ extension SwiftLanguageServer {
485485
response: dict, for: snapshot, compileCommand: compileCmd)
486486
}
487487

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

Sources/SourceKitLSP/ToolchainLanguageServer.swift

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

7575
/// Sent when the `BuildSystem` has detected that dependencies of the given file have changed
7676
/// (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)