diff --git a/Sources/SKCore/BuildServerBuildSystem.swift b/Sources/SKCore/BuildServerBuildSystem.swift index 361e1a981..01561d721 100644 --- a/Sources/SKCore/BuildServerBuildSystem.swift +++ b/Sources/SKCore/BuildServerBuildSystem.swift @@ -279,6 +279,16 @@ extension BuildServerBuildSystem: BuildSystem { return [ConfiguredTarget(targetID: "dummy", runDestinationID: "dummy")] } + public func generateBuildGraph() {} + + public func topologicalSort(of targets: [ConfiguredTarget]) async -> [ConfiguredTarget]? { + return nil + } + + public func prepare(targets: [ConfiguredTarget]) async throws { + throw PrepareNotSupportedError() + } + public func registerForChangeNotifications(for uri: DocumentURI) { let request = RegisterForChanges(uri: uri, action: .register) _ = self.buildServer?.send(request) { result in diff --git a/Sources/SKCore/BuildSystem.swift b/Sources/SKCore/BuildSystem.swift index 03b2bdad9..f6880a00b 100644 --- a/Sources/SKCore/BuildSystem.swift +++ b/Sources/SKCore/BuildSystem.swift @@ -31,14 +31,19 @@ public struct SourceFileInfo: Sendable { /// The URI of the source file. public let uri: DocumentURI + /// `true` if this file belongs to the root project that the user is working on. It is false, if the file belongs + /// to a dependency of the project. + public let isPartOfRootProject: Bool + /// Whether the file might contain test cases. This property is an over-approximation. It might be true for files /// from non-test targets or files that don't actually contain any tests. Keeping this list of files with /// `mayContainTets` minimal as possible helps reduce the amount of work that the syntactic test indexer needs to /// perform. public let mayContainTests: Bool - public init(uri: DocumentURI, mayContainTests: Bool) { + public init(uri: DocumentURI, isPartOfRootProject: Bool, mayContainTests: Bool) { self.uri = uri + self.isPartOfRootProject = isPartOfRootProject self.mayContainTests = mayContainTests } } @@ -64,6 +69,13 @@ public struct ConfiguredTarget: Hashable, Sendable { } } +/// An error build systems can throw from `prepare` if they don't support preparation of targets. +public struct PrepareNotSupportedError: Error, CustomStringConvertible { + public init() {} + + public var description: String { "Preparation not supported" } +} + /// Provider of FileBuildSettings and other build-related information. /// /// The primary role of the build system is to answer queries for @@ -114,6 +126,22 @@ public protocol BuildSystem: AnyObject, Sendable { /// Return the list of targets and run destinations that the given document can be built for. func configuredTargets(for document: DocumentURI) async -> [ConfiguredTarget] + /// Re-generate the build graph including all the tasks that are necessary for building the entire build graph, like + /// resolving package versions. + func generateBuildGraph() async throws + + /// Sort the targets so that low-level targets occur before high-level targets. + /// + /// This sorting is best effort but allows the indexer to prepare and index low-level targets first, which allows + /// index data to be available earlier. + /// + /// `nil` if the build system doesn't support topological sorting of targets. + func topologicalSort(of targets: [ConfiguredTarget]) async -> [ConfiguredTarget]? + + /// Prepare the given targets for indexing and semantic functionality. This should build all swift modules of target + /// dependencies. + func prepare(targets: [ConfiguredTarget]) async throws + /// If the build system has knowledge about the language that this document should be compiled in, return it. /// /// This is used to determine the language in which a source file should be background indexed. @@ -146,5 +174,3 @@ public protocol BuildSystem: AnyObject, Sendable { /// The callback might also be called without an actual change to `sourceFiles`. func addSourceFilesDidChangeCallback(_ callback: @Sendable @escaping () async -> Void) async } - -public let buildTargetsNotSupported = ResponseError.methodNotFound(BuildTargets.method) diff --git a/Sources/SKCore/BuildSystemManager.swift b/Sources/SKCore/BuildSystemManager.swift index bc83e7830..8c6c49d30 100644 --- a/Sources/SKCore/BuildSystemManager.swift +++ b/Sources/SKCore/BuildSystemManager.swift @@ -208,6 +208,18 @@ extension BuildSystemManager { return settings } + public func generateBuildGraph() async throws { + try await self.buildSystem?.generateBuildGraph() + } + + public func topologicalSort(of targets: [ConfiguredTarget]) async throws -> [ConfiguredTarget]? { + return await buildSystem?.topologicalSort(of: targets) + } + + public func prepare(targets: [ConfiguredTarget]) async throws { + try await buildSystem?.prepare(targets: targets) + } + public func registerForChangeNotifications(for uri: DocumentURI, language: Language) async { logger.debug("registerForChangeNotifications(\(uri.forLogging))") let mainFile = await mainFile(for: uri, language: language) @@ -247,7 +259,7 @@ extension BuildSystemManager { public func testFiles() async -> [DocumentURI] { return await sourceFiles().compactMap { (info: SourceFileInfo) -> DocumentURI? in - guard info.mayContainTests else { + guard info.isPartOfRootProject, info.mayContainTests else { return nil } return info.uri diff --git a/Sources/SKCore/CompilationDatabaseBuildSystem.swift b/Sources/SKCore/CompilationDatabaseBuildSystem.swift index b8eaf03b9..eab796d52 100644 --- a/Sources/SKCore/CompilationDatabaseBuildSystem.swift +++ b/Sources/SKCore/CompilationDatabaseBuildSystem.swift @@ -125,6 +125,16 @@ extension CompilationDatabaseBuildSystem: BuildSystem { return [ConfiguredTarget(targetID: "dummy", runDestinationID: "dummy")] } + public func prepare(targets: [ConfiguredTarget]) async throws { + throw PrepareNotSupportedError() + } + + public func generateBuildGraph() {} + + public func topologicalSort(of targets: [ConfiguredTarget]) -> [ConfiguredTarget]? { + return nil + } + public func registerForChangeNotifications(for uri: DocumentURI) async { self.watchedFiles.insert(uri) } @@ -208,7 +218,7 @@ extension CompilationDatabaseBuildSystem: BuildSystem { return [] } return compdb.allCommands.map { - SourceFileInfo(uri: DocumentURI($0.url), mayContainTests: true) + SourceFileInfo(uri: DocumentURI($0.url), isPartOfRootProject: true, mayContainTests: true) } } diff --git a/Sources/SKSupport/Collection+PartitionIntoBatches.swift b/Sources/SKSupport/Collection+PartitionIntoBatches.swift index ed1effdc9..674c11cff 100644 --- a/Sources/SKSupport/Collection+PartitionIntoBatches.swift +++ b/Sources/SKSupport/Collection+PartitionIntoBatches.swift @@ -10,7 +10,7 @@ // //===----------------------------------------------------------------------===// -public extension Collection { +public extension Collection where Index == Int { /// Partition the elements of the collection into `numberOfBatches` roughly equally sized batches. /// /// Elements are assigned to the batches round-robin. This ensures that elements that are close to each other in the @@ -32,4 +32,18 @@ public extension Collection { } return batches.filter { !$0.isEmpty } } + + /// Partition the collection into batches that have a maximum size of `batchSize`. + /// + /// The last batch will contain the remainder elements. + func partition(intoBatchesOfSize batchSize: Int) -> [[Element]] { + var batches: [[Element]] = [] + batches.reserveCapacity(self.count / batchSize) + var lastIndex = self.startIndex + for index in stride(from: self.startIndex, to: self.endIndex, by: batchSize).dropFirst() + [self.endIndex] { + batches.append(Array(self[lastIndex..>! = nil + /// A `ObservabilitySystem` from `SwiftPM` that logs. + private let observabilitySystem = ObservabilitySystem({ scope, diagnostic in + logger.log(level: diagnostic.severity.asLogLevel, "SwiftPM log: \(diagnostic.description)") + }) + + /// Whether the SwiftPMBuildSystem may modify `Package.resolved` or not. + /// + /// This is `false` if the `SwiftPMBuildSystem` is pointed at a `.index-build` directory that's independent of the + /// user's build. In this case `SwiftPMBuildSystem` is allowed to clone repositories even if no `Package.resolved` + /// exists. + private let forceResolvedVersions: Bool + /// Creates a build system using the Swift Package Manager, if this workspace is a package. /// /// - Parameters: @@ -132,11 +148,13 @@ public actor SwiftPMBuildSystem { toolchainRegistry: ToolchainRegistry, fileSystem: FileSystem = localFileSystem, buildSetup: BuildSetup, + forceResolvedVersions: Bool, reloadPackageStatusCallback: @escaping (ReloadPackageStatus) async -> Void = { _ in } ) async throws { self.workspacePath = workspacePath self.fileSystem = fileSystem self.toolchainRegistry = toolchainRegistry + self.forceResolvedVersions = forceResolvedVersions guard let packageRoot = findPackageDirectory(containing: workspacePath, fileSystem) else { throw Error.noManifest(workspacePath: workspacePath) @@ -204,7 +222,6 @@ public actor SwiftPMBuildSystem { } await delegate.filesDependenciesUpdated(filesWithUpdatedDependencies) } - try await reloadPackage() } @@ -217,6 +234,7 @@ public actor SwiftPMBuildSystem { url: URL, toolchainRegistry: ToolchainRegistry, buildSetup: BuildSetup, + forceResolvedVersions: Bool, reloadPackageStatusCallback: @escaping (ReloadPackageStatus) async -> Void ) async { do { @@ -225,6 +243,7 @@ public actor SwiftPMBuildSystem { toolchainRegistry: toolchainRegistry, fileSystem: localFileSystem, buildSetup: buildSetup, + forceResolvedVersions: forceResolvedVersions, reloadPackageStatusCallback: reloadPackageStatusCallback ) } catch Error.noManifest { @@ -237,6 +256,9 @@ public actor SwiftPMBuildSystem { } extension SwiftPMBuildSystem { + public func generateBuildGraph() async throws { + try await self.reloadPackage() + } /// (Re-)load the package settings by parsing the manifest and resolving all the targets and /// dependencies. @@ -248,13 +270,9 @@ extension SwiftPMBuildSystem { } } - let observabilitySystem = ObservabilitySystem({ scope, diagnostic in - logger.log(level: diagnostic.severity.asLogLevel, "SwiftPM log: \(diagnostic.description)") - }) - let modulesGraph = try self.workspace.loadPackageGraph( rootInput: PackageGraphRootInput(packages: [AbsolutePath(projectRoot)]), - forceResolvedVersions: true, + forceResolvedVersions: forceResolvedVersions, observabilityScope: observabilitySystem.topScope ) @@ -272,7 +290,15 @@ extension SwiftPMBuildSystem { /// with only some properties modified. self.modulesGraph = modulesGraph - self.targets = try buildDescription.allTargetsInTopologicalOrder(in: modulesGraph) + self.targets = Dictionary( + try buildDescription.allTargetsInTopologicalOrder(in: modulesGraph).enumerated().map { (index, target) in + return (key: target.name, (index, target)) + }, + uniquingKeysWith: { first, second in + logger.fault("Found two targets with the same name \(first.buildTarget.name)") + return second + } + ) self.fileToTarget = [AbsolutePath: SwiftBuildTarget]( modulesGraph.allTargets.flatMap { target in @@ -343,14 +369,8 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem { return try settings(forPackageManifest: path) } - let buildTargets = self.targets.filter({ $0.name == configuredTarget.targetID }) - if buildTargets.count > 1 { - logger.error("Found multiple targets with name \(configuredTarget.targetID). Picking the first one") - } - guard let buildTarget = buildTargets.first else { - if buildTargets.isEmpty { - logger.error("Did not find target with name \(configuredTarget.targetID)") - } + guard let buildTarget = self.targets[configuredTarget.targetID]?.buildTarget else { + logger.error("Did not find target with name \(configuredTarget.targetID)") return nil } @@ -368,7 +388,8 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem { } public func defaultLanguage(for document: DocumentURI) async -> Language? { - // TODO (indexing): Query The SwiftPM build system for the document's language + // TODO (indexing): Query The SwiftPM build system for the document's language. + // https://github.com/apple/sourcekit-lsp/issues/1267 return nil } @@ -395,6 +416,77 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem { return [] } + public func topologicalSort(of targets: [ConfiguredTarget]) -> [ConfiguredTarget]? { + return targets.sorted { (lhs: ConfiguredTarget, rhs: ConfiguredTarget) -> Bool in + let lhsIndex = self.targets[lhs.targetID]?.index ?? self.targets.count + let rhsIndex = self.targets[lhs.targetID]?.index ?? self.targets.count + return lhsIndex < rhsIndex + } + } + + public func prepare(targets: [ConfiguredTarget]) async throws { + // TODO (indexing): Support preparation of multiple targets at once. + // https://github.com/apple/sourcekit-lsp/issues/1262 + for target in targets { + try await prepare(singleTarget: target) + } + } + + private func prepare(singleTarget target: ConfiguredTarget) async throws { + // TODO (indexing): Add a proper 'prepare' job in SwiftPM instead of building the target. + // https://github.com/apple/sourcekit-lsp/issues/1254 + guard let toolchain = await toolchainRegistry.default else { + logger.error("Not preparing because not toolchain exists") + return + } + guard let swift = toolchain.swift else { + logger.error( + "Not preparing because toolchain at \(toolchain.identifier) does not contain a Swift compiler" + ) + return + } + let arguments = [ + swift.pathString, "build", + "--scratch-path", self.workspace.location.scratchDirectory.pathString, + "--disable-index-store", + "--target", target.targetID, + ] + let process = Process( + arguments: arguments, + workingDirectory: workspacePath + ) + try process.launch() + let result = try await process.waitUntilExitSendingSigIntOnTaskCancellation() + switch result.exitStatus.exhaustivelySwitchable { + case .terminated(code: 0): + break + case .terminated(code: let code): + // This most likely happens if there are compilation errors in the source file. This is nothing to worry about. + let stdout = (try? String(bytes: result.output.get(), encoding: .utf8)) ?? "" + let stderr = (try? String(bytes: result.stderrOutput.get(), encoding: .utf8)) ?? "" + logger.debug( + """ + Preparation of target \(target.targetID) terminated with non-zero exit code \(code) + Stderr: + \(stderr) + Stdout: + \(stdout) + """ + ) + case .signalled(signal: let signal): + if !Task.isCancelled { + // The indexing job finished with a signal. Could be because the compiler crashed. + // Ignore signal exit codes if this task has been cancelled because the compiler exits with SIGINT if it gets + // interrupted. + logger.error("Preparation of target \(target.targetID) signaled \(signal)") + } + case .abnormal(exception: let exception): + if !Task.isCancelled { + logger.error("Preparation of target \(target.targetID) exited abnormally \(exception)") + } + } + } + public func registerForChangeNotifications(for uri: DocumentURI) async { self.watchedFiles.insert(uri) } @@ -489,14 +581,11 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem { public func sourceFiles() -> [SourceFileInfo] { return fileToTarget.compactMap { (path, target) -> SourceFileInfo? in - guard target.isPartOfRootPackage else { - // Don't consider files from package dependencies as possible test files. - return nil - } // We should only set mayContainTests to `true` for files from test targets // (https://github.com/apple/sourcekit-lsp/issues/1174). return SourceFileInfo( uri: DocumentURI(path.asURL), + isPartOfRootProject: target.isPartOfRootPackage, mayContainTests: true ) } diff --git a/Sources/SKTestSupport/FileManager+findFiles.swift b/Sources/SKTestSupport/FileManager+findFiles.swift index 232d158a2..b4bf60d12 100644 --- a/Sources/SKTestSupport/FileManager+findFiles.swift +++ b/Sources/SKTestSupport/FileManager+findFiles.swift @@ -24,4 +24,16 @@ extension FileManager { } return result } + + /// Returns the URLs of all files with the given file name in the given directory (recursively). + public func findFiles(named name: String, in directory: URL) -> [URL] { + var result: [URL] = [] + let enumerator = self.enumerator(at: directory, includingPropertiesForKeys: nil) + while let url = enumerator?.nextObject() as? URL { + if url.lastPathComponent == name { + result.append(url) + } + } + return result + } } diff --git a/Sources/SKTestSupport/SwiftPMDependencyProject.swift b/Sources/SKTestSupport/SwiftPMDependencyProject.swift index c35c7ddd9..496834c5c 100644 --- a/Sources/SKTestSupport/SwiftPMDependencyProject.swift +++ b/Sources/SKTestSupport/SwiftPMDependencyProject.swift @@ -71,13 +71,13 @@ public class SwiftPMDependencyProject { var files = files files["Package.swift"] = manifest - for (fileLocation, contents) in files { + for (fileLocation, markedContents) in files { let fileURL = fileLocation.url(relativeTo: packageDirectory) try FileManager.default.createDirectory( at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true ) - try contents.write(to: fileURL, atomically: true, encoding: .utf8) + try extractMarkers(markedContents).textWithoutMarkers.write(to: fileURL, atomically: true, encoding: .utf8) } try await runGitCommand(["init"], workingDirectory: packageDirectory) diff --git a/Sources/SemanticIndex/CMakeLists.txt b/Sources/SemanticIndex/CMakeLists.txt index 2d805df8c..197bfde6e 100644 --- a/Sources/SemanticIndex/CMakeLists.txt +++ b/Sources/SemanticIndex/CMakeLists.txt @@ -1,6 +1,8 @@ add_library(SemanticIndex STATIC CheckedIndex.swift + IndexTaskDescription.swift + PreparationTaskDescription.swift SemanticIndexManager.swift UpdateIndexStoreTaskDescription.swift ) diff --git a/Sources/SemanticIndex/IndexTaskDescription.swift b/Sources/SemanticIndex/IndexTaskDescription.swift new file mode 100644 index 000000000..e2e2c21f2 --- /dev/null +++ b/Sources/SemanticIndex/IndexTaskDescription.swift @@ -0,0 +1,105 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SKCore + +/// A task that either prepares targets or updates the index store for a set of files. +public enum IndexTaskDescription: TaskDescriptionProtocol { + case updateIndexStore(UpdateIndexStoreTaskDescription) + case preparation(PreparationTaskDescription) + + public var isIdempotent: Bool { + switch self { + case .updateIndexStore(let taskDescription): return taskDescription.isIdempotent + case .preparation(let taskDescription): return taskDescription.isIdempotent + } + } + + public var estimatedCPUCoreCount: Int { + switch self { + case .updateIndexStore(let taskDescription): return taskDescription.estimatedCPUCoreCount + case .preparation(let taskDescription): return taskDescription.estimatedCPUCoreCount + } + } + + public var id: String { + switch self { + case .updateIndexStore(let taskDescription): return "indexing-\(taskDescription.id)" + case .preparation(let taskDescription): return "preparation-\(taskDescription.id)" + } + } + + public var description: String { + switch self { + case .updateIndexStore(let taskDescription): return taskDescription.description + case .preparation(let taskDescription): return taskDescription.description + } + } + + public var redactedDescription: String { + switch self { + case .updateIndexStore(let taskDescription): return taskDescription.redactedDescription + case .preparation(let taskDescription): return taskDescription.redactedDescription + } + } + + public func execute() async { + switch self { + case .updateIndexStore(let taskDescription): return await taskDescription.execute() + case .preparation(let taskDescription): return await taskDescription.execute() + } + } + + /// Forward to the underlying task to compute the dependencies. Preparation and index tasks don't have any + /// dependencies that are managed by `TaskScheduler`. `SemanticIndexManager` awaits the preparation of a target before + /// indexing files within it. + public func dependencies( + to currentlyExecutingTasks: [IndexTaskDescription] + ) -> [TaskDependencyAction] { + switch self { + case .updateIndexStore(let taskDescription): + let currentlyExecutingTasks = + currentlyExecutingTasks + .compactMap { (currentlyExecutingTask) -> UpdateIndexStoreTaskDescription? in + if case .updateIndexStore(let currentlyExecutingTask) = currentlyExecutingTask { + return currentlyExecutingTask + } + return nil + } + return taskDescription.dependencies(to: currentlyExecutingTasks).map { + switch $0 { + case .waitAndElevatePriorityOfDependency(let td): + return .waitAndElevatePriorityOfDependency(.updateIndexStore(td)) + case .cancelAndRescheduleDependency(let td): + return .cancelAndRescheduleDependency(.updateIndexStore(td)) + } + } + case .preparation(let taskDescription): + let currentlyExecutingTasks = + currentlyExecutingTasks + .compactMap { (currentlyExecutingTask) -> PreparationTaskDescription? in + if case .preparation(let currentlyExecutingTask) = currentlyExecutingTask { + return currentlyExecutingTask + } + return nil + } + return taskDescription.dependencies(to: currentlyExecutingTasks).map { + switch $0 { + case .waitAndElevatePriorityOfDependency(let td): + return .waitAndElevatePriorityOfDependency(.preparation(td)) + case .cancelAndRescheduleDependency(let td): + return .cancelAndRescheduleDependency(.preparation(td)) + } + } + } + } +} diff --git a/Sources/SemanticIndex/PreparationTaskDescription.swift b/Sources/SemanticIndex/PreparationTaskDescription.swift new file mode 100644 index 000000000..d2348b272 --- /dev/null +++ b/Sources/SemanticIndex/PreparationTaskDescription.swift @@ -0,0 +1,110 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import CAtomics +import Foundation +import LSPLogging +import LanguageServerProtocol +import SKCore + +import struct TSCBasic.AbsolutePath +import class TSCBasic.Process + +private var preparationIDForLogging = AtomicUInt32(initialValue: 1) + +/// Describes a task to prepare a set of targets. +/// +/// This task description can be scheduled in a `TaskScheduler`. +public struct PreparationTaskDescription: TaskDescriptionProtocol { + public let id = preparationIDForLogging.fetchAndIncrement() + + /// The targets that should be prepared. + private let targetsToPrepare: [ConfiguredTarget] + + /// The build system manager that is used to get the toolchain and build settings for the files to index. + private let buildSystemManager: BuildSystemManager + + /// A callback that is called when the task finishes. + /// + /// Intended for testing purposes. + private let didFinishCallback: @Sendable (PreparationTaskDescription) -> Void + + /// The task is idempotent because preparing the same target twice produces the same result as preparing it once. + public var isIdempotent: Bool { true } + + public var estimatedCPUCoreCount: Int { 1 } + + public var description: String { + return self.redactedDescription + } + + public var redactedDescription: String { + return "preparation-\(id)" + } + + init( + targetsToPrepare: [ConfiguredTarget], + buildSystemManager: BuildSystemManager, + didFinishCallback: @escaping @Sendable (PreparationTaskDescription) -> Void + ) { + self.targetsToPrepare = targetsToPrepare + self.buildSystemManager = buildSystemManager + self.didFinishCallback = didFinishCallback + } + + public func execute() async { + defer { + didFinishCallback(self) + } + // Only use the last two digits of the preparation ID for the logging scope to avoid creating too many scopes. + // See comment in `withLoggingScope`. + // The last 2 digits should be sufficient to differentiate between multiple concurrently running preparation operations + await withLoggingScope("preparation-\(id % 100)") { + let startDate = Date() + let targetsToPrepare = targetsToPrepare.sorted(by: { + ($0.targetID, $0.runDestinationID) < ($1.targetID, $1.runDestinationID) + }) + let targetsToPrepareDescription = + targetsToPrepare + .map { "\($0.targetID)-\($0.runDestinationID)" } + .joined(separator: ", ") + logger.log( + "Starting preparation with priority \(Task.currentPriority.rawValue, privacy: .public): \(targetsToPrepareDescription)" + ) + do { + try await buildSystemManager.prepare(targets: targetsToPrepare) + } catch { + logger.error( + "Preparation failed: \(error.forLogging)" + ) + } + logger.log( + "Finished preparation in \(Date().timeIntervalSince(startDate) * 1000, privacy: .public)ms: \(targetsToPrepareDescription)" + ) + } + } + + public func dependencies( + to currentlyExecutingTasks: [PreparationTaskDescription] + ) -> [TaskDependencyAction] { + return currentlyExecutingTasks.compactMap { (other) -> TaskDependencyAction? in + if other.targetsToPrepare.count > self.targetsToPrepare.count { + // If there is an prepare operation with more targets already running, suspend it. + // The most common use case for this is if we prepare all targets simultaneously during the initial preparation + // when a project is opened and need a single target indexed for user interaction. We should suspend the + // workspace-wide preparation and just prepare the currently needed target. + return .cancelAndRescheduleDependency(other) + } + return .waitAndElevatePriorityOfDependency(other) + } + } +} diff --git a/Sources/SemanticIndex/SemanticIndexManager.swift b/Sources/SemanticIndex/SemanticIndexManager.swift index 057a13b8f..4b6ed22a8 100644 --- a/Sources/SemanticIndex/SemanticIndexManager.swift +++ b/Sources/SemanticIndex/SemanticIndexManager.swift @@ -37,23 +37,27 @@ public final actor SemanticIndexManager { /// Files that have never been indexed are not in this dictionary. private var indexStatus: [DocumentURI: FileIndexStatus] = [:] + /// The task to generate the build graph (resolving package dependencies, generating the build description, + /// ...). `nil` if no build graph is currently being generated. + private var generateBuildGraphTask: Task? + /// The `TaskScheduler` that manages the scheduling of index tasks. This is shared among all `SemanticIndexManager`s /// in the process, to ensure that we don't schedule more index operations than processor cores from multiple /// workspaces. - private let indexTaskScheduler: TaskScheduler + private let indexTaskScheduler: TaskScheduler /// Callback that is called when an index task has finished. /// /// Currently only used for testing. - private let indexTaskDidFinish: (@Sendable (UpdateIndexStoreTaskDescription) -> Void)? + private let indexTaskDidFinish: (@Sendable (IndexTaskDescription) -> Void)? // MARK: - Public API public init( index: UncheckedIndex, buildSystemManager: BuildSystemManager, - indexTaskScheduler: TaskScheduler, - indexTaskDidFinish: (@Sendable (UpdateIndexStoreTaskDescription) -> Void)? + indexTaskScheduler: TaskScheduler, + indexTaskDidFinish: (@Sendable (IndexTaskDescription) -> Void)? ) { self.index = index.checked(for: .modifiedFiles) self.buildSystemManager = buildSystemManager @@ -65,13 +69,27 @@ public final actor SemanticIndexManager { /// Returns immediately after scheduling that task. /// /// Indexing is being performed with a low priority. - public func scheduleBackgroundIndex(files: some Collection) { - self.index(files: files, priority: .low) + public func scheduleBackgroundIndex(files: some Collection) async { + await self.index(files: files, priority: .low) + } + + /// Regenerate the build graph (also resolving package dependencies) and then index all the source files known to the + /// build system. + public func scheduleBuildGraphGenerationAndBackgroundIndexAllFiles() async { + generateBuildGraphTask = Task(priority: .low) { + await orLog("Generating build graph") { try await self.buildSystemManager.generateBuildGraph() } + await scheduleBackgroundIndex(files: await self.buildSystemManager.sourceFiles().map(\.uri)) + generateBuildGraphTask = nil + } } /// Wait for all in-progress index tasks to finish. public func waitForUpToDateIndex() async { logger.info("Waiting for up-to-date index") + // Wait for a build graph update first, if one is in progress. This will add all index tasks to `indexStatus`, so we + // can await the index tasks below. + await generateBuildGraphTask?.value + await withTaskGroup(of: Void.self) { taskGroup in for (_, status) in indexStatus { switch status { @@ -97,6 +115,10 @@ public final actor SemanticIndexManager { logger.info( "Waiting for up-to-date index for \(uris.map { $0.fileURL?.lastPathComponent ?? $0.stringValue }.joined(separator: ", "))" ) + // If there's a build graph update in progress wait for that to finish so we can discover new files in the build + // system. + await generateBuildGraphTask?.value + // Create a new index task for the files that aren't up-to-date. The newly scheduled index tasks will // - Wait for the existing index operations to finish if they have the same number of files. // - Reschedule the background index task in favor of an index task with fewer source files. @@ -107,41 +129,112 @@ public final actor SemanticIndexManager { // MARK: - Helper functions + /// Prepare the given targets for indexing + private func prepare(targets: [ConfiguredTarget], priority: TaskPriority?) async { + await self.indexTaskScheduler.schedule( + priority: priority, + .preparation( + PreparationTaskDescription( + targetsToPrepare: targets, + buildSystemManager: self.buildSystemManager, + didFinishCallback: { [weak self] taskDescription in + self?.indexTaskDidFinish?(.preparation(taskDescription)) + } + ) + ) + ).value + } + + /// Update the index store for the given files, assuming that their targets have already been prepared. + private func updateIndexStore(for files: [DocumentURI], priority: TaskPriority?) async { + await self.indexTaskScheduler.schedule( + priority: priority, + .updateIndexStore( + UpdateIndexStoreTaskDescription( + filesToIndex: Set(files), + buildSystemManager: self.buildSystemManager, + index: self.index, + didFinishCallback: { [weak self] taskDescription in + self?.indexTaskDidFinish?(.updateIndexStore(taskDescription)) + } + ) + ) + ).value + for file in files { + self.indexStatus[file] = .upToDate + } + } + /// Index the given set of files at the given priority. /// /// The returned task finishes when all files are indexed. @discardableResult - private func index(files: some Collection, priority: TaskPriority?) -> Task { + private func index(files: some Collection, priority: TaskPriority?) async -> Task { let outOfDateFiles = files.filter { if case .upToDate = indexStatus[$0] { return false } return true } + .sorted(by: { $0.stringValue < $1.stringValue }) // sort files to get deterministic indexing order + + // Sort the targets in topological order so that low-level targets get built before high-level targets, allowing us + // to index the low-level targets ASAP. + var filesByTarget: [ConfiguredTarget: [DocumentURI]] = [:] + for file in outOfDateFiles { + guard let target = await buildSystemManager.canonicalConfiguredTarget(for: file) else { + logger.error("Not indexing \(file.forLogging) because the target could not be determined") + continue + } + filesByTarget[target, default: []].append(file) + } + + var sortedTargets: [ConfiguredTarget] = + await orLog("Sorting targets") { try await buildSystemManager.topologicalSort(of: Array(filesByTarget.keys)) } + ?? Array(filesByTarget.keys).sorted(by: { + ($0.targetID, $0.runDestinationID) < ($1.targetID, $1.runDestinationID) + }) + + if Set(sortedTargets) != Set(filesByTarget.keys) { + logger.fault( + """ + Sorting targets topologically changed set of targets: + \(sortedTargets.map(\.targetID).joined(separator: ", ")) != \(filesByTarget.keys.map(\.targetID).joined(separator: ", ")) + """ + ) + sortedTargets = Array(filesByTarget.keys).sorted(by: { + ($0.targetID, $0.runDestinationID) < ($1.targetID, $1.runDestinationID) + }) + } var indexTasks: [Task] = [] - // TODO (indexing): Group index operations by target when we support background preparation. - for files in outOfDateFiles.partition(intoNumberOfBatches: ProcessInfo.processInfo.processorCount * 5) { + // TODO (indexing): When we can index multiple targets concurrently in SwiftPM, increase the batch size to half the + // processor count, so we can get parallelism during preparation. + // https://github.com/apple/sourcekit-lsp/issues/1262 + for targetsBatch in sortedTargets.partition(intoBatchesOfSize: 1) { let indexTask = Task(priority: priority) { - await self.indexTaskScheduler.schedule( - priority: priority, - UpdateIndexStoreTaskDescription( - filesToIndex: Set(files), - buildSystemManager: self.buildSystemManager, - index: self.index, - didFinishCallback: { [weak self] taskDescription in - self?.indexTaskDidFinish?(taskDescription) + // First prepare the targets. + await prepare(targets: targetsBatch, priority: priority) + + // And after preparation is done, index the files in the targets. + await withTaskGroup(of: Void.self) { taskGroup in + for target in targetsBatch { + // TODO (indexing): Once swiftc supports indexing of multiple files in a single invocation, increase the + // batch size to allow it to share AST builds between multiple files within a target. + // https://github.com/apple/sourcekit-lsp/issues/1268 + for fileBatch in filesByTarget[target]!.partition(intoBatchesOfSize: 1) { + taskGroup.addTask { + await self.updateIndexStore(for: fileBatch, priority: priority) + } } - ) - ).value - for file in files { - indexStatus[file] = .upToDate + } + await taskGroup.waitForAll() } } indexTasks.append(indexTask) - for file in files { + for file in targetsBatch.flatMap({ filesByTarget[$0]! }) { indexStatus[file] = .inProgress(indexTask) } } @@ -150,7 +243,7 @@ public final actor SemanticIndexManager { return Task(priority: priority) { await withTaskGroup(of: Void.self) { taskGroup in for indexTask in indexTasksImmutable { - taskGroup.addTask(priority: priority) { + taskGroup.addTask { await indexTask.value } } diff --git a/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift b/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift index 9af7a5081..ac3f491af 100644 --- a/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift +++ b/Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift @@ -87,6 +87,7 @@ public struct UpdateIndexStoreTaskDescription: TaskDescriptionProtocol { let filesToIndex = filesToIndex.sorted(by: { $0.stringValue < $1.stringValue }) // TODO (indexing): Once swiftc supports it, we should group files by target and index files within the same // target together in one swiftc invocation. + // https://github.com/apple/sourcekit-lsp/issues/1268 for file in filesToIndex { await updateIndexStoreForSingleFile(file) } @@ -163,6 +164,7 @@ public struct UpdateIndexStoreTaskDescription: TaskDescriptionProtocol { } case .c, .cpp, .objective_c, .objective_cpp: // TODO (indexing): Support indexing of clang files, including headers. + // https://github.com/apple/sourcekit-lsp/issues/1253 break default: logger.error( diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index f925eb9bb..3c4f67518 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -453,7 +453,7 @@ public actor SourceKitLSPServer { /// /// Shared process-wide to ensure the scheduled index operations across multiple workspaces don't exceed the maximum /// number of processor cores that the user allocated to background indexing. - private let indexTaskScheduler: TaskScheduler + private let indexTaskScheduler: TaskScheduler private var packageLoadingWorkDoneProgress = WorkDoneProgressState( "SourceKitLSP.SourceKitLSPServer.reloadPackage", diff --git a/Sources/SourceKitLSP/Workspace.swift b/Sources/SourceKitLSP/Workspace.swift index 5306b66f4..565d53fea 100644 --- a/Sources/SourceKitLSP/Workspace.swift +++ b/Sources/SourceKitLSP/Workspace.swift @@ -89,7 +89,7 @@ public final class Workspace { underlyingBuildSystem: BuildSystem?, index uncheckedIndex: UncheckedIndex?, indexDelegate: SourceKitIndexDelegate?, - indexTaskScheduler: TaskScheduler + indexTaskScheduler: TaskScheduler ) async { self.documentManager = documentManager self.buildSetup = options.buildSetup @@ -123,8 +123,8 @@ public final class Workspace { } // Trigger an initial population of `syntacticTestIndex`. await syntacticTestIndex.listOfTestFilesDidChange(buildSystemManager.testFiles()) - if let semanticIndexManager, let underlyingBuildSystem { - await semanticIndexManager.scheduleBackgroundIndex(files: await underlyingBuildSystem.sourceFiles().map(\.uri)) + if let semanticIndexManager { + await semanticIndexManager.scheduleBuildGraphGenerationAndBackgroundIndexAllFiles() } } @@ -142,21 +142,24 @@ public final class Workspace { options: SourceKitLSPServer.Options, compilationDatabaseSearchPaths: [RelativePath], indexOptions: IndexOptions = IndexOptions(), - indexTaskScheduler: TaskScheduler, + indexTaskScheduler: TaskScheduler, reloadPackageStatusCallback: @escaping (ReloadPackageStatus) async -> Void ) async throws { var buildSystem: BuildSystem? = nil if let rootUrl = rootUri.fileURL, let rootPath = try? AbsolutePath(validating: rootUrl.path) { var options = options + var forceResolvedVersions = true if options.indexOptions.enableBackgroundIndexing, options.buildSetup.path == nil { options.buildSetup.path = rootPath.appending(component: ".index-build") + forceResolvedVersions = false } func createSwiftPMBuildSystem(rootUrl: URL) async -> SwiftPMBuildSystem? { return await SwiftPMBuildSystem( url: rootUrl, toolchainRegistry: toolchainRegistry, buildSetup: options.buildSetup, + forceResolvedVersions: forceResolvedVersions, reloadPackageStatusCallback: reloadPackageStatusCallback ) } @@ -295,7 +298,7 @@ public struct IndexOptions { /// A callback that is called when an index task finishes. /// /// Intended for testing purposes. - public var indexTaskDidFinish: (@Sendable (UpdateIndexStoreTaskDescription) -> Void)? + public var indexTaskDidFinish: (@Sendable (IndexTaskDescription) -> Void)? public init( indexStorePath: AbsolutePath? = nil, @@ -304,7 +307,7 @@ public struct IndexOptions { listenToUnitEvents: Bool = true, enableBackgroundIndexing: Bool = false, maxCoresPercentageToUseForBackgroundIndexing: Double = 1, - indexTaskDidFinish: (@Sendable (UpdateIndexStoreTaskDescription) -> Void)? = nil + indexTaskDidFinish: (@Sendable (IndexTaskDescription) -> Void)? = nil ) { self.indexStorePath = indexStorePath self.indexDatabasePath = indexDatabasePath diff --git a/Tests/SKCoreTests/BuildSystemManagerTests.swift b/Tests/SKCoreTests/BuildSystemManagerTests.swift index 09a201cd6..8baa896ca 100644 --- a/Tests/SKCoreTests/BuildSystemManagerTests.swift +++ b/Tests/SKCoreTests/BuildSystemManagerTests.swift @@ -457,6 +457,16 @@ class ManualBuildSystem: BuildSystem { return [ConfiguredTarget(targetID: "dummy", runDestinationID: "dummy")] } + public func prepare(targets: [ConfiguredTarget]) async throws { + throw PrepareNotSupportedError() + } + + public func generateBuildGraph() {} + + public func topologicalSort(of targets: [ConfiguredTarget]) -> [ConfiguredTarget]? { + return nil + } + func registerForChangeNotifications(for uri: DocumentURI) async { } diff --git a/Tests/SKSwiftPMWorkspaceTests/SwiftPMBuildSystemTests.swift b/Tests/SKSwiftPMWorkspaceTests/SwiftPMBuildSystemTests.swift index 900915f7e..2ad593636 100644 --- a/Tests/SKSwiftPMWorkspaceTests/SwiftPMBuildSystemTests.swift +++ b/Tests/SKSwiftPMWorkspaceTests/SwiftPMBuildSystemTests.swift @@ -53,7 +53,8 @@ final class SwiftPMBuildSystemTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup + buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup, + forceResolvedVersions: true ) ) } @@ -80,7 +81,8 @@ final class SwiftPMBuildSystemTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup + buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup, + forceResolvedVersions: true ) ) } @@ -107,7 +109,8 @@ final class SwiftPMBuildSystemTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: ToolchainRegistry(toolchains: []), fileSystem: fs, - buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup + buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup, + forceResolvedVersions: true ) ) } @@ -134,7 +137,8 @@ final class SwiftPMBuildSystemTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup + buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup, + forceResolvedVersions: true ) let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") @@ -202,7 +206,8 @@ final class SwiftPMBuildSystemTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: config + buildSetup: config, + forceResolvedVersions: true ) let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") @@ -239,7 +244,8 @@ final class SwiftPMBuildSystemTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup + buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup, + forceResolvedVersions: true ) let source = try resolveSymlinks(packageRoot.appending(component: "Package.swift")) @@ -272,7 +278,8 @@ final class SwiftPMBuildSystemTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup + buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup, + forceResolvedVersions: true ) let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") @@ -317,7 +324,8 @@ final class SwiftPMBuildSystemTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup + buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup, + forceResolvedVersions: true ) let aswift = packageRoot.appending(components: "Sources", "libA", "a.swift") @@ -379,7 +387,8 @@ final class SwiftPMBuildSystemTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup + buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup, + forceResolvedVersions: true ) let aswift = packageRoot.appending(components: "Sources", "libA", "a.swift") @@ -419,7 +428,8 @@ final class SwiftPMBuildSystemTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup + buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup, + forceResolvedVersions: true ) let acxx = packageRoot.appending(components: "Sources", "lib", "a.cpp") @@ -498,7 +508,8 @@ final class SwiftPMBuildSystemTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: ToolchainRegistry.forTesting, fileSystem: fs, - buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup + buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup, + forceResolvedVersions: true ) let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") @@ -545,7 +556,8 @@ final class SwiftPMBuildSystemTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup + buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup, + forceResolvedVersions: true ) let aswift1 = packageRoot.appending(components: "Sources", "lib", "a.swift") @@ -609,7 +621,8 @@ final class SwiftPMBuildSystemTests: XCTestCase { workspacePath: symlinkRoot, toolchainRegistry: ToolchainRegistry.forTesting, fileSystem: fs, - buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup + buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup, + forceResolvedVersions: true ) for file in [acpp, ah] { @@ -649,7 +662,8 @@ final class SwiftPMBuildSystemTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup + buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup, + forceResolvedVersions: true ) let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") @@ -685,7 +699,8 @@ final class SwiftPMBuildSystemTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup + buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup, + forceResolvedVersions: true ) assertEqual(await swiftpmBuildSystem.projectRoot, try resolveSymlinks(tempDir.appending(component: "pkg"))) @@ -721,7 +736,8 @@ final class SwiftPMBuildSystemTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup + buildSetup: SourceKitLSPServer.Options.testDefault.buildSetup, + forceResolvedVersions: true ) let aswift = packageRoot.appending(components: "Plugins", "MyPlugin", "a.swift") diff --git a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift index 22b2af822..a0ee73ff1 100644 --- a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift +++ b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift @@ -105,6 +105,64 @@ final class BackgroundIndexingTests: XCTestCase { ) } + func testBackgroundIndexingOfMultiModuleProject() async throws { + try await SkipUnless.swiftpmStoresModulesInSubdirectory() + let project = try await SwiftPMTestProject( + files: [ + "LibA/MyFile.swift": """ + public func 1️⃣foo() {} + """, + "LibB/MyOtherFile.swift": """ + import LibA + func 2️⃣bar() { + 3️⃣foo() + } + """, + ], + manifest: """ + // swift-tools-version: 5.7 + + import PackageDescription + + let package = Package( + name: "MyLibrary", + targets: [ + .target(name: "LibA"), + .target(name: "LibB", dependencies: ["LibA"]), + ] + ) + """, + serverOptions: backgroundIndexingOptions + ) + + let (uri, positions) = try project.openDocument("MyFile.swift") + let prepare = try await project.testClient.send( + CallHierarchyPrepareRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"]) + ) + let initialItem = try XCTUnwrap(prepare?.only) + let calls = try await project.testClient.send(CallHierarchyIncomingCallsRequest(item: initialItem)) + XCTAssertEqual( + calls, + [ + CallHierarchyIncomingCall( + from: CallHierarchyItem( + name: "bar()", + kind: .function, + tags: nil, + uri: try project.uri(for: "MyOtherFile.swift"), + range: Range(try project.position(of: "2️⃣", in: "MyOtherFile.swift")), + selectionRange: Range(try project.position(of: "2️⃣", in: "MyOtherFile.swift")), + data: .dictionary([ + "usr": .string("s:4LibB3baryyF"), + "uri": .string(try project.uri(for: "MyOtherFile.swift").stringValue), + ]) + ), + fromRanges: [Range(try project.position(of: "3️⃣", in: "MyOtherFile.swift"))] + ) + ] + ) + } + func testBackgroundIndexingHappensWithLowPriority() async throws { var serverOptions = backgroundIndexingOptions serverOptions.indexOptions.indexTaskDidFinish = { taskDescription in @@ -152,4 +210,78 @@ final class BackgroundIndexingTests: XCTestCase { } semaphore.wait() } + + func testBackgroundIndexingOfPackageDependency() async throws { + try await SkipUnless.swiftpmStoresModulesInSubdirectory() + let dependencyContents = """ + public func 1️⃣doSomething() {} + """ + + let dependencyProject = try await SwiftPMDependencyProject(files: [ + "Sources/MyDependency/MyDependency.swift": dependencyContents + ]) + defer { dependencyProject.keepAlive() } + + let project = try await SwiftPMTestProject( + files: [ + "Test.swift": """ + import MyDependency + + func 2️⃣test() { + 3️⃣doSomething() + } + """ + ], + manifest: """ + // swift-tools-version: 5.7 + import PackageDescription + let package = Package( + name: "MyLibrary", + dependencies: [.package(url: "\(dependencyProject.packageDirectory)", from: "1.0.0")], + targets: [ + .target( + name: "MyLibrary", + dependencies: [.product(name: "MyDependency", package: "MyDependency")] + ) + ] + ) + """, + serverOptions: backgroundIndexingOptions + ) + + let dependencyUrl = try XCTUnwrap( + FileManager.default.findFiles(named: "MyDependency.swift", in: project.scratchDirectory).only + ) + let dependencyUri = DocumentURI(dependencyUrl) + let testFileUri = try project.uri(for: "Test.swift") + let positions = project.testClient.openDocument(dependencyContents, uri: dependencyUri) + let prepare = try await project.testClient.send( + CallHierarchyPrepareRequest(textDocument: TextDocumentIdentifier(dependencyUri), position: positions["1️⃣"]) + ) + + let calls = try await project.testClient.send( + CallHierarchyIncomingCallsRequest(item: try XCTUnwrap(prepare?.only)) + ) + + XCTAssertEqual( + calls, + [ + CallHierarchyIncomingCall( + from: CallHierarchyItem( + name: "test()", + kind: .function, + tags: nil, + uri: testFileUri, + range: try project.range(from: "2️⃣", to: "2️⃣", in: "Test.swift"), + selectionRange: try project.range(from: "2️⃣", to: "2️⃣", in: "Test.swift"), + data: .dictionary([ + "usr": .string("s:9MyLibrary4testyyF"), + "uri": .string(testFileUri.stringValue), + ]) + ), + fromRanges: [try project.range(from: "3️⃣", to: "3️⃣", in: "Test.swift")] + ) + ] + ) + } } diff --git a/Tests/SourceKitLSPTests/BuildSystemTests.swift b/Tests/SourceKitLSPTests/BuildSystemTests.swift index 252abea30..0116b962f 100644 --- a/Tests/SourceKitLSPTests/BuildSystemTests.swift +++ b/Tests/SourceKitLSPTests/BuildSystemTests.swift @@ -55,6 +55,16 @@ final class TestBuildSystem: BuildSystem { return [ConfiguredTarget(targetID: "dummy", runDestinationID: "dummy")] } + public func prepare(targets: [ConfiguredTarget]) async throws { + throw PrepareNotSupportedError() + } + + public func generateBuildGraph() {} + + public func topologicalSort(of targets: [ConfiguredTarget]) -> [ConfiguredTarget]? { + return nil + } + func registerForChangeNotifications(for uri: DocumentURI) async { watchedFiles.insert(uri) }