Skip to content

Commit b32d2e2

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 6713141 commit b32d2e2

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
}
@@ -259,13 +253,21 @@ extension BuildSystemManager: BuildSystemDelegate {
259253
}
260254
}
261255

262-
public func fileBuildSettingsChangedImpl(_ changedFiles: Set<DocumentURI>) async {
263-
let changedWatchedFiles = changedFiles.flatMap({ mainFile in
264-
self.watchedFiles.filter { $1.mainFile == mainFile }.keys
256+
private func watchedFilesReferencing(mainFiles: Set<DocumentURI>) -> Set<DocumentURI> {
257+
return Set(watchedFiles.compactMap { (watchedFile, mainFileAndLanguage) in
258+
if mainFiles.contains(mainFileAndLanguage.mainFile) {
259+
return watchedFile
260+
} else {
261+
return nil
262+
}
265263
})
264+
}
265+
266+
public func fileBuildSettingsChangedImpl(_ changedFiles: Set<DocumentURI>) async {
267+
let changedWatchedFiles = watchedFilesReferencing(mainFiles: changedFiles)
266268

267269
if !changedWatchedFiles.isEmpty, let delegate = self._delegate {
268-
await delegate.fileBuildSettingsChanged(Set(changedWatchedFiles))
270+
await delegate.fileBuildSettingsChanged(changedWatchedFiles)
269271
}
270272
}
271273

@@ -286,10 +288,9 @@ extension BuildSystemManager: BuildSystemDelegate {
286288
}
287289

288290
// Need to map the changed main files back into changed watch files.
289-
let changedWatchedFiles = self.watchedFiles.filter { changedFiles.contains($1.mainFile) }
290-
let newChangedFiles = Set(changedWatchedFiles.map { $0.key })
291-
if let delegate = self._delegate, !newChangedFiles.isEmpty {
292-
await delegate.filesDependenciesUpdated(newChangedFiles)
291+
let changedWatchedFiles = watchedFilesReferencing(mainFiles: changedFiles)
292+
if let delegate, !changedWatchedFiles.isEmpty {
293+
await delegate.filesDependenciesUpdated(changedWatchedFiles)
293294
}
294295
}
295296

@@ -329,28 +330,63 @@ extension BuildSystemManager: MainFilesDelegate {
329330
}
330331

331332
// FIXME: Consider debouncing/limiting this, seems to trigger often during a build.
333+
/// Checks if there are any files in `mainFileAssociations` where the main file
334+
/// that we have stored has changed.
335+
///
336+
/// For all of these files, re-associate the file with the new main file and
337+
/// inform the delegate that the build settings for it might have changed.
332338
public func mainFilesChangedImpl() async {
333-
let origWatched = self.watchedFiles
334-
self.watchedFiles = [:]
335-
var buildSettingsChanges = Set<DocumentURI>()
336-
337-
for (uri, state) in origWatched {
338-
let mainFiles = self._mainFilesProvider?.mainFilesContainingFile(uri) ?? []
339-
let newMainFile = chooseMainFile(for: uri, previous: state.mainFile, from: mainFiles)
340-
let language = state.language
341-
342-
self.watchedFiles[uri] = (newMainFile, language)
343-
344-
if state.mainFile != newMainFile {
345-
log("main file for '\(uri)' changed old: '\(state.mainFile)' -> new: '\(newMainFile)'", level: .info)
346-
await self.checkUnreferencedMainFile(state.mainFile)
339+
var changedMainFileAssociations: Set<DocumentURI> = []
340+
for (file, (oldMainFile, language)) in self.watchedFiles {
341+
let newMainFile = self.mainFile(for: file, useCache: false)
342+
if newMainFile != oldMainFile {
343+
self.watchedFiles[file] = (newMainFile, language)
344+
changedMainFileAssociations.insert(file)
345+
}
346+
}
347347

348-
buildSettingsChanges.insert(uri)
348+
for file in changedMainFileAssociations {
349+
guard let language = watchedFiles[file]?.language else {
350+
continue
349351
}
352+
// Re-register for notifications of this file within the build system.
353+
// This is the easiest way to make sure we are watching for build setting
354+
// changes of the new main file and stop watching for build setting
355+
// changes in the old main file if no other watched file depends on it.
356+
await self.unregisterForChangeNotifications(for: file)
357+
await self.registerForChangeNotifications(for: file, language: language)
350358
}
351359

352-
if let delegate = self._delegate, !buildSettingsChanges.isEmpty {
353-
await delegate.fileBuildSettingsChanged(buildSettingsChanges)
360+
if let delegate, !changedMainFileAssociations.isEmpty {
361+
await delegate.fileBuildSettingsChanged(changedMainFileAssociations)
362+
}
363+
}
364+
365+
/// Return the main file that should be used to get build settings for `uri`.
366+
///
367+
/// For Swift or normal C files, this will be the file itself. For header
368+
/// files, we pick a main file that includes the header since header files
369+
/// don't have build settings by themselves.
370+
private func mainFile(for uri: DocumentURI, useCache: Bool = true) -> DocumentURI {
371+
if useCache, let mainFile = self.watchedFiles[uri]?.mainFile {
372+
// Performance optimization: We did already compute the main file and have
373+
// it cached. We can just return it.
374+
return mainFile
375+
}
376+
guard let mainFilesProvider else {
377+
return uri
378+
}
379+
380+
let mainFiles = mainFilesProvider.mainFilesContainingFile(uri)
381+
if mainFiles.contains(uri) {
382+
// If the main files contain the file itself, prefer to use that one
383+
return uri
384+
} else if let mainFile = mainFiles.sorted(by: { $0.pseudoPath < $1.pseudoPath }).first {
385+
// Pick the lexicographically first main file if it exists.
386+
// This makes sure that picking a main file is deterministic.
387+
return mainFile
388+
} else {
389+
return uri
354390
}
355391
}
356392
}
@@ -359,23 +395,6 @@ extension BuildSystemManager {
359395

360396
/// *For Testing* Returns the main file used for `uri`, if this is a registered file.
361397
public func _cachedMainFile(for uri: DocumentURI) -> DocumentURI? {
362-
watchedFiles[uri]?.mainFile
363-
}
364-
}
365-
366-
/// Choose a new main file for the given uri, preferring to use a previous main file if still
367-
/// available, to avoid thrashing the settings unnecessarily, and falling back to `uri` itself if
368-
/// there are no main files found at all.
369-
private func chooseMainFile(
370-
for uri: DocumentURI,
371-
previous: DocumentURI? = nil,
372-
from mainFiles: Set<DocumentURI>) -> DocumentURI
373-
{
374-
if let previous = previous, mainFiles.contains(previous) {
375-
return previous
376-
} else if mainFiles.isEmpty || mainFiles.contains(uri) {
377-
return uri
378-
} else {
379-
return mainFiles.first!
398+
return self.watchedFiles[uri]?.mainFile
380399
}
381400
}

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.mainFilesChangedImpl()
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.mainFilesChangedImpl()
279278
try await fulfillmentOfOrThrow([changed3], timeout: 1)
280279

0 commit comments

Comments
 (0)