Skip to content

Commit 0cccd3b

Browse files
committed
Refactor the computation of main files
The motivating change for this was to deterministically pick a main file for a header file instead of picking the first element in a set, which is not deterministic. While doing this, I also changed the main file computation to not carry any state about previous main files around. If a header file is associated with a main file b.cpp and a new a.cpp gets added that imports the header as well, we should be using a.cpp for the build settings. That way we will get the same build settings if we close and re-open the project. And this was a good opportunity to refactor some of the main file handling into smaller, more dedicated functions.
1 parent 453ebfb commit 0cccd3b

File tree

2 files changed

+91
-73
lines changed

2 files changed

+91
-73
lines changed

Sources/SKCore/BuildSystemManager.swift

Lines changed: 87 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ extension MainFileStatus {
8686
/// this class has a configurable `buildSettings` timeout which denotes the amount of time to give
8787
/// the build system before applying the fallback arguments.
8888
public actor BuildSystemManager {
89-
/// The set of watched files, along with their main file and language.
89+
/// The files for which the delegate has requested change notifications, ie.
90+
/// the files for which the delegate wants to get `filesDependenciesUpdated`
91+
/// callbacks if the file's build settings.
9092
var watchedFiles: [DocumentURI: (mainFile: DocumentURI, language: Language)] = [:]
9193

9294
/// The underlying primary build system.
@@ -181,29 +183,25 @@ extension BuildSystemManager {
181183
for document: DocumentURI,
182184
language: Language
183185
) async -> (buildSettings: FileBuildSettings, isFallback: Bool)? {
184-
if let mainFile = mainFilesProvider?.mainFilesContainingFile(document).first {
185-
if let mainFileBuildSettings = await buildSettings(for: mainFile, language: language) {
186-
return (
187-
buildSettings: mainFileBuildSettings.buildSettings.patching(newFile: document.pseudoPath, originalFile: mainFile.pseudoPath),
188-
isFallback: mainFileBuildSettings.isFallback
189-
)
190-
}
186+
let mainFile = mainFile(for: document)
187+
var buildSettings = await buildSettings(for: mainFile, language: language)
188+
if mainFile != document, let settings = buildSettings?.buildSettings {
189+
// If the main file isn't the file itself, we need to patch the build settings
190+
// to reference `document` instead of `mainFile`.
191+
buildSettings?.buildSettings = settings.patching(newFile: document.pseudoPath, originalFile: mainFile.pseudoPath)
191192
}
192-
return await buildSettings(for: document, language: language)
193+
return buildSettings
193194
}
194195

195196
public func registerForChangeNotifications(for uri: DocumentURI, language: Language) async {
196197
log("registerForChangeNotifications(\(uri.pseudoPath))")
197-
let mainFile: DocumentURI
198-
199-
if let watchedFile = self.watchedFiles[uri] {
200-
mainFile = watchedFile.mainFile
201-
} else {
202-
let mainFiles = self._mainFilesProvider?.mainFilesContainingFile(uri)
203-
mainFile = chooseMainFile(for: uri, from: mainFiles ?? [])
204-
self.watchedFiles[uri] = (mainFile, language)
205-
}
198+
let mainFile = mainFile(for: uri)
199+
self.watchedFiles[uri] = (mainFile, language)
206200

201+
// Register for change notifications of the main file in the underlying build
202+
// system. That way, iff the main file changes, we will also notify the
203+
// delegate about build setting changes of all header files that are based
204+
// on that main file.
207205
await buildSystem?.registerForChangeNotifications(for: mainFile, language: language)
208206
}
209207

@@ -231,14 +229,10 @@ extension BuildSystemManager {
231229
return
232230
}
233231
self.watchedFiles[uri] = nil
234-
await self.checkUnreferencedMainFile(mainFile)
235-
}
236232

237-
/// If the given main file is no longer referenced by any watched files,
238-
/// remove it and unregister it at the underlying build system.
239-
func checkUnreferencedMainFile(_ mainFile: DocumentURI) async {
240-
if !self.watchedFiles.values.lazy.map({ $0.mainFile }).contains(mainFile) {
241-
// This was the last reference to the main file. Remove it.
233+
if watchedFilesReferencing(mainFiles: [mainFile]).isEmpty {
234+
// Nobody is interested in this main file anymore.
235+
// We are no longer interested in change notifications for it.
242236
await self.buildSystem?.unregisterForChangeNotifications(for: mainFile)
243237
}
244238
}
@@ -252,13 +246,21 @@ extension BuildSystemManager {
252246
}
253247

254248
extension BuildSystemManager: BuildSystemDelegate {
255-
public func fileBuildSettingsChanged(_ changedFiles: Set<DocumentURI>) async {
256-
let changedWatchedFiles = changedFiles.flatMap({ mainFile in
257-
self.watchedFiles.filter { $1.mainFile == mainFile }.keys
249+
private func watchedFilesReferencing(mainFiles: Set<DocumentURI>) -> Set<DocumentURI> {
250+
return Set(watchedFiles.compactMap { (watchedFile, mainFileAndLanguage) in
251+
if mainFiles.contains(mainFileAndLanguage.mainFile) {
252+
return watchedFile
253+
} else {
254+
return nil
255+
}
258256
})
257+
}
258+
259+
public func fileBuildSettingsChanged(_ changedFiles: Set<DocumentURI>) async {
260+
let changedWatchedFiles = watchedFilesReferencing(mainFiles: changedFiles)
259261

260262
if !changedWatchedFiles.isEmpty, let delegate = self._delegate {
261-
await delegate.fileBuildSettingsChanged(Set(changedWatchedFiles))
263+
await delegate.fileBuildSettingsChanged(changedWatchedFiles)
262264
}
263265
}
264266

@@ -272,10 +274,9 @@ extension BuildSystemManager: BuildSystemDelegate {
272274
}
273275

274276
// Need to map the changed main files back into changed watch files.
275-
let changedWatchedFiles = self.watchedFiles.filter { changedFiles.contains($1.mainFile) }
276-
let newChangedFiles = Set(changedWatchedFiles.map { $0.key })
277-
if let delegate = self._delegate, !newChangedFiles.isEmpty {
278-
await delegate.filesDependenciesUpdated(newChangedFiles)
277+
let changedWatchedFiles = watchedFilesReferencing(mainFiles: changedFiles)
278+
if let delegate, !changedWatchedFiles.isEmpty {
279+
await delegate.filesDependenciesUpdated(changedWatchedFiles)
279280
}
280281
}
281282

@@ -294,28 +295,63 @@ extension BuildSystemManager: BuildSystemDelegate {
294295

295296
extension BuildSystemManager: MainFilesDelegate {
296297
// FIXME: Consider debouncing/limiting this, seems to trigger often during a build.
298+
/// Checks if there are any files in `mainFileAssociations` where the main file
299+
/// that we have stored has changed.
300+
///
301+
/// For all of these files, re-associate the file with the new main file and
302+
/// inform the delegate that the build settings for it might have changed.
297303
public func mainFilesChanged() async {
298-
let origWatched = self.watchedFiles
299-
self.watchedFiles = [:]
300-
var buildSettingsChanges = Set<DocumentURI>()
301-
302-
for (uri, state) in origWatched {
303-
let mainFiles = self._mainFilesProvider?.mainFilesContainingFile(uri) ?? []
304-
let newMainFile = chooseMainFile(for: uri, previous: state.mainFile, from: mainFiles)
305-
let language = state.language
306-
307-
self.watchedFiles[uri] = (newMainFile, language)
308-
309-
if state.mainFile != newMainFile {
310-
log("main file for '\(uri)' changed old: '\(state.mainFile)' -> new: '\(newMainFile)'", level: .info)
311-
await self.checkUnreferencedMainFile(state.mainFile)
304+
var changedMainFileAssociations: Set<DocumentURI> = []
305+
for (file, (oldMainFile, language)) in self.watchedFiles {
306+
let newMainFile = self.mainFile(for: file, useCache: false)
307+
if newMainFile != oldMainFile {
308+
self.watchedFiles[file] = (newMainFile, language)
309+
changedMainFileAssociations.insert(file)
310+
}
311+
}
312312

313-
buildSettingsChanges.insert(uri)
313+
for file in changedMainFileAssociations {
314+
guard let language = watchedFiles[file]?.language else {
315+
continue
314316
}
317+
// Re-register for notifications of this file within the build system.
318+
// This is the easiest way to make sure we are watching for build setting
319+
// changes of the new main file and stop watching for build setting
320+
// changes in the old main file if no other watched file depends on it.
321+
await self.unregisterForChangeNotifications(for: file)
322+
await self.registerForChangeNotifications(for: file, language: language)
315323
}
316324

317-
if let delegate = self._delegate, !buildSettingsChanges.isEmpty {
318-
await delegate.fileBuildSettingsChanged(buildSettingsChanges)
325+
if let delegate, !changedMainFileAssociations.isEmpty {
326+
await delegate.fileBuildSettingsChanged(changedMainFileAssociations)
327+
}
328+
}
329+
330+
/// Return the main file that should be used to get build settings for `uri`.
331+
///
332+
/// For Swift or normal C files, this will be the file itself. For header
333+
/// files, we pick a main file that includes the header since header files
334+
/// don't have build settings by themselves.
335+
private func mainFile(for uri: DocumentURI, useCache: Bool = true) -> DocumentURI {
336+
if useCache, let mainFile = self.watchedFiles[uri]?.mainFile {
337+
// Performance optimization: We did already compute the main file and have
338+
// it cached. We can just return it.
339+
return mainFile
340+
}
341+
guard let mainFilesProvider else {
342+
return uri
343+
}
344+
345+
let mainFiles = mainFilesProvider.mainFilesContainingFile(uri)
346+
if mainFiles.contains(uri) {
347+
// If the main files contain the file itself, prefer to use that one
348+
return uri
349+
} else if let mainFile = mainFiles.min(by: { $0.pseudoPath < $1.pseudoPath }) {
350+
// Pick the lexicographically first main file if it exists.
351+
// This makes sure that picking a main file is deterministic.
352+
return mainFile
353+
} else {
354+
return uri
319355
}
320356
}
321357
}
@@ -324,23 +360,6 @@ extension BuildSystemManager {
324360

325361
/// *For Testing* Returns the main file used for `uri`, if this is a registered file.
326362
public func _cachedMainFile(for uri: DocumentURI) -> DocumentURI? {
327-
watchedFiles[uri]?.mainFile
328-
}
329-
}
330-
331-
/// Choose a new main file for the given uri, preferring to use a previous main file if still
332-
/// available, to avoid thrashing the settings unnecessarily, and falling back to `uri` itself if
333-
/// there are no main files found at all.
334-
private func chooseMainFile(
335-
for uri: DocumentURI,
336-
previous: DocumentURI? = nil,
337-
from mainFiles: Set<DocumentURI>) -> DocumentURI
338-
{
339-
if let previous = previous, mainFiles.contains(previous) {
340-
return previous
341-
} else if mainFiles.isEmpty || mainFiles.contains(uri) {
342-
return uri
343-
} else {
344-
return mainFiles.first!
363+
return self.watchedFiles[uri]?.mainFile
345364
}
346365
}

Tests/SKCoreTests/BuildSystemManagerTests.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,13 @@ final class BuildSystemManagerTests: XCTestCase {
6969
await bsm.mainFilesChanged()
7070

7171
await assertEqual(bsm._cachedMainFile(for: a), a)
72-
await assertEqual(bsm._cachedMainFile(for: b), bMain) // never changes to a
72+
await assertEqual(bsm._cachedMainFile(for: b), a)
7373
await assertEqual(bsm._cachedMainFile(for: c), c)
7474
await assertEqual(bsm._cachedMainFile(for: d), d)
7575

7676
await bsm.unregisterForChangeNotifications(for: a)
7777
await assertEqual(bsm._cachedMainFile(for: a), nil)
78-
await assertEqual(bsm._cachedMainFile(for: b), bMain) // never changes to a
78+
await assertEqual(bsm._cachedMainFile(for: b), a)
7979
await assertEqual(bsm._cachedMainFile(for: c), c)
8080
await assertEqual(bsm._cachedMainFile(for: d), d)
8181

@@ -272,9 +272,8 @@ final class BuildSystemManagerTests: XCTestCase {
272272

273273
mainFiles.mainFiles[h] = Set([cpp1, cpp2])
274274

275-
let changed3 = expectation(description: "added main file, no update")
276-
changed3.isInverted = true
277-
await del.setExpected([(h, .c, nil, changed3, #file, #line)])
275+
let changed3 = expectation(description: "added lexicographically earlier main file")
276+
await del.setExpected([(h, .c, bs.map[cpp1]!, changed3, #file, #line)])
278277
await bsm.mainFilesChanged()
279278
try await fulfillmentOfOrThrow([changed3], timeout: 1)
280279

0 commit comments

Comments
 (0)