Skip to content

Commit 81dec01

Browse files
authored
Merge pull request #847 from ahoppen/ahoppen/refactor-main-file-computation
Refactor the computation of main files
2 parents d6101a1 + 0cccd3b commit 81dec01

File tree

3 files changed

+95
-92
lines changed

3 files changed

+95
-92
lines changed

Sources/SKCore/BuildSystemManager.swift

Lines changed: 88 additions & 71 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.
@@ -120,7 +122,6 @@ public actor BuildSystemManager {
120122

121123
public func filesDidChange(_ events: [FileEvent]) async {
122124
await self.buildSystem?.filesDidChange(events)
123-
self.fallbackBuildSystem?.filesDidChange(events)
124125
}
125126
}
126127

@@ -182,31 +183,26 @@ extension BuildSystemManager {
182183
for document: DocumentURI,
183184
language: Language
184185
) async -> (buildSettings: FileBuildSettings, isFallback: Bool)? {
185-
if let mainFile = mainFilesProvider?.mainFilesContainingFile(document).first {
186-
if let mainFileBuildSettings = await buildSettings(for: mainFile, language: language) {
187-
return (
188-
buildSettings: mainFileBuildSettings.buildSettings.patching(newFile: document.pseudoPath, originalFile: mainFile.pseudoPath),
189-
isFallback: mainFileBuildSettings.isFallback
190-
)
191-
}
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)
192192
}
193-
return await buildSettings(for: document, language: language)
193+
return buildSettings
194194
}
195195

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

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.
208205
await buildSystem?.registerForChangeNotifications(for: mainFile, language: language)
209-
fallbackBuildSystem?.registerForChangeNotifications(for: mainFile, language: language)
210206
}
211207

212208
/// Return settings for `file` based on the `change` settings for `mainFile`.
@@ -233,34 +229,38 @@ extension BuildSystemManager {
233229
return
234230
}
235231
self.watchedFiles[uri] = nil
236-
await self.checkUnreferencedMainFile(mainFile)
237-
}
238232

239-
/// If the given main file is no longer referenced by any watched files,
240-
/// remove it and unregister it at the underlying build system.
241-
func checkUnreferencedMainFile(_ mainFile: DocumentURI) async {
242-
if !self.watchedFiles.values.lazy.map({ $0.mainFile }).contains(mainFile) {
243-
// 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.
244236
await self.buildSystem?.unregisterForChangeNotifications(for: mainFile)
245237
}
246238
}
247239

248240
public func fileHandlingCapability(for uri: DocumentURI) async -> FileHandlingCapability {
249241
return max(
250242
await buildSystem?.fileHandlingCapability(for: uri) ?? .unhandled,
251-
fallbackBuildSystem?.fileHandlingCapability(for: uri) ?? .unhandled
243+
fallbackBuildSystem != nil ? .fallback : .unhandled
252244
)
253245
}
254246
}
255247

256248
extension BuildSystemManager: BuildSystemDelegate {
257-
public func fileBuildSettingsChanged(_ changedFiles: Set<DocumentURI>) async {
258-
let changedWatchedFiles = changedFiles.flatMap({ mainFile in
259-
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+
}
260256
})
257+
}
258+
259+
public func fileBuildSettingsChanged(_ changedFiles: Set<DocumentURI>) async {
260+
let changedWatchedFiles = watchedFilesReferencing(mainFiles: changedFiles)
261261

262262
if !changedWatchedFiles.isEmpty, let delegate = self._delegate {
263-
await delegate.fileBuildSettingsChanged(Set(changedWatchedFiles))
263+
await delegate.fileBuildSettingsChanged(changedWatchedFiles)
264264
}
265265
}
266266

@@ -274,10 +274,9 @@ extension BuildSystemManager: BuildSystemDelegate {
274274
}
275275

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

@@ -296,28 +295,63 @@ extension BuildSystemManager: BuildSystemDelegate {
296295

297296
extension BuildSystemManager: MainFilesDelegate {
298297
// 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.
299303
public func mainFilesChanged() async {
300-
let origWatched = self.watchedFiles
301-
self.watchedFiles = [:]
302-
var buildSettingsChanges = Set<DocumentURI>()
303-
304-
for (uri, state) in origWatched {
305-
let mainFiles = self._mainFilesProvider?.mainFilesContainingFile(uri) ?? []
306-
let newMainFile = chooseMainFile(for: uri, previous: state.mainFile, from: mainFiles)
307-
let language = state.language
308-
309-
self.watchedFiles[uri] = (newMainFile, language)
310-
311-
if state.mainFile != newMainFile {
312-
log("main file for '\(uri)' changed old: '\(state.mainFile)' -> new: '\(newMainFile)'", level: .info)
313-
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+
}
314312

315-
buildSettingsChanges.insert(uri)
313+
for file in changedMainFileAssociations {
314+
guard let language = watchedFiles[file]?.language else {
315+
continue
316316
}
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)
317323
}
318324

319-
if let delegate = self._delegate, !buildSettingsChanges.isEmpty {
320-
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
321355
}
322356
}
323357
}
@@ -326,23 +360,6 @@ extension BuildSystemManager {
326360

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

Sources/SKCore/FallbackBuildSystem.swift

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import class TSCBasic.Process
2121
import struct TSCBasic.AbsolutePath
2222

2323
/// A simple BuildSystem suitable as a fallback when accurate settings are unknown.
24-
public final class FallbackBuildSystem: BuildSystem {
24+
public final class FallbackBuildSystem {
2525

2626
let buildSetup: BuildSetup
2727

@@ -59,13 +59,6 @@ public final class FallbackBuildSystem: BuildSystem {
5959
}
6060
}
6161

62-
public func registerForChangeNotifications(for uri: DocumentURI, language: Language) {
63-
// Fallback build systems never change.
64-
}
65-
66-
/// We don't support change watching.
67-
public func unregisterForChangeNotifications(for: DocumentURI) {}
68-
6962
func settingsSwift(_ file: String) -> FileBuildSettings {
7063
var args: [String] = []
7164
args.append(contentsOf: self.buildSetup.flags.swiftCompilerFlags)
@@ -98,10 +91,4 @@ public final class FallbackBuildSystem: BuildSystem {
9891
args.append(file)
9992
return FileBuildSettings(compilerArguments: args)
10093
}
101-
102-
public func filesDidChange(_ events: [FileEvent]) {}
103-
104-
public func fileHandlingCapability(for uri: DocumentURI) -> FileHandlingCapability {
105-
return .fallback
106-
}
10794
}

Tests/SKCoreTests/BuildSystemManagerTests.swift

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ final class BuildSystemManagerTests: XCTestCase {
3434
]
3535

3636
let bsm = await BuildSystemManager(
37-
buildSystem: FallbackBuildSystem(buildSetup: .default),
38-
fallbackBuildSystem: nil,
37+
buildSystem: nil,
38+
fallbackBuildSystem: FallbackBuildSystem(buildSetup: .default),
3939
mainFilesProvider: mainFiles)
4040
defer { withExtendedLifetime(bsm) {} } // Keep BSM alive for callbacks.
4141

@@ -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)