Skip to content

Commit 51a035d

Browse files
committed
Use the BSP output file paths request to exclude units from the index that are no longer part of any targets
1 parent c3ecb9a commit 51a035d

File tree

8 files changed

+164
-44
lines changed

8 files changed

+164
-44
lines changed

Contributor Documentation/BSP Extensions.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ If `data` contains a string value for the `workDoneProgressTitle` key, then the
5050

5151
For all the source files in this target, the output paths that are used during indexing, ie. the `-index-unit-output-path` for the file, if it is specified in the compiler arguments or the file that is passed as `-o`, if `-index-unit-output-path` is not specified.
5252

53-
server communicates during the initialize handshake whether this method is supported or not by setting `outputPathsProvider: true` in `SourceKitInitializeBuildResponseData`.
53+
This allows SourceKit-LSP to remove index entries for source files that are removed from a target but remain present on disk.
54+
55+
The server communicates during the initialize handshake whether this method is supported or not by setting `outputPathsProvider: true` in `SourceKitInitializeBuildResponseData`.
5456

5557
- method: `buildTarget/outputPaths`
5658
- params: `OutputPathsParams`

Sources/BuildServerProtocol/Messages/InitializeBuildRequest.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -323,15 +323,18 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send
323323
if case .string(let indexStorePath) = dictionary[CodingKeys.indexStorePath.stringValue] {
324324
self.indexStorePath = indexStorePath
325325
}
326-
if let watchers = dictionary[CodingKeys.watchers.stringValue] {
327-
self.watchers = [FileSystemWatcher](fromLSPArray: watchers)
326+
if case .bool(let outputPathsProvider) = dictionary[CodingKeys.outputPathsProvider.stringValue] {
327+
self.outputPathsProvider = outputPathsProvider
328328
}
329329
if case .bool(let prepareProvider) = dictionary[CodingKeys.prepareProvider.stringValue] {
330330
self.prepareProvider = prepareProvider
331331
}
332332
if case .bool(let sourceKitOptionsProvider) = dictionary[CodingKeys.sourceKitOptionsProvider.stringValue] {
333333
self.sourceKitOptionsProvider = sourceKitOptionsProvider
334334
}
335+
if let watchers = dictionary[CodingKeys.watchers.stringValue] {
336+
self.watchers = [FileSystemWatcher](fromLSPArray: watchers)
337+
}
335338
}
336339

337340
public func encodeToLSPAny() -> LanguageServerProtocol.LSPAny {
@@ -342,15 +345,18 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send
342345
if let indexStorePath {
343346
result[CodingKeys.indexStorePath.stringValue] = .string(indexStorePath)
344347
}
345-
if let watchers {
346-
result[CodingKeys.watchers.stringValue] = watchers.encodeToLSPAny()
348+
if let outputPathsProvider {
349+
result[CodingKeys.outputPathsProvider.stringValue] = .bool(outputPathsProvider)
347350
}
348351
if let prepareProvider {
349352
result[CodingKeys.prepareProvider.stringValue] = .bool(prepareProvider)
350353
}
351354
if let sourceKitOptionsProvider {
352355
result[CodingKeys.sourceKitOptionsProvider.stringValue] = .bool(sourceKitOptionsProvider)
353356
}
357+
if let watchers {
358+
result[CodingKeys.watchers.stringValue] = watchers.encodeToLSPAny()
359+
}
354360
return .dictionary(result)
355361
}
356362
}

Sources/BuildSystemIntegration/BuildSystemManager.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,7 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
631631
// All targets might have changed
632632
return true
633633
}
634-
return !updatedTargets.intersection(cacheKey.targets).isEmpty
634+
return !updatedTargets.isDisjoint(with: cacheKey.targets)
635635
}
636636
self.cachedSourceFilesAndDirectories.clearAll(isolation: self)
637637

@@ -1167,7 +1167,17 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
11671167
return response.items
11681168
}
11691169

1170-
package func outputPath(from targets: Set<BuildTargetIdentifier>) async throws -> [String] {
1170+
/// Return the output paths for all source files known to the build server.
1171+
///
1172+
/// See `BuildTargetOutputPathsRequest` for details.
1173+
package func outputPathInAllTargets() async throws -> [String] {
1174+
return try await outputPaths(in: Set(buildTargets().map(\.key)))
1175+
}
1176+
1177+
/// For all source files in the given targets, return their output file paths.
1178+
///
1179+
/// See `BuildTargetOutputPathsRequest` for details.
1180+
package func outputPaths(in targets: Set<BuildTargetIdentifier>) async throws -> [String] {
11711181
guard let buildSystemAdapter = await buildSystemAdapterAfterInitialized, !targets.isEmpty else {
11721182
return []
11731183
}

Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem {
466466
signposter.endInterval("Reloading package", state)
467467
}
468468

469-
package nonisolated var supportsPreparationAndOutputPaths: Bool { true }
469+
package nonisolated var supportsPreparationAndOutputPaths: Bool { options.backgroundIndexingOrDefault }
470470

471471
package var buildPath: URL {
472472
return destinationBuildParameters.buildPath.asURL
@@ -489,11 +489,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem {
489489

490490
/// Return the compiler arguments for the given source file within a target, making any necessary adjustments to
491491
/// account for differences in the SwiftPM versions being linked into SwiftPM and being installed in the toolchain.
492-
private func compilerArguments(
493-
for file: DocumentURI,
494-
in buildTarget: any SwiftBuildTarget,
495-
language: Language
496-
) async throws -> [String] {
492+
private func compilerArguments(for file: DocumentURI, in buildTarget: any SwiftBuildTarget) async throws -> [String] {
497493
guard let fileURL = file.fileURL else {
498494
struct NonFileURIError: Swift.Error, CustomStringConvertible {
499495
let uri: DocumentURI
@@ -638,11 +634,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem {
638634
// getting its compiler arguments and then patching up the compiler arguments by replacing the substitute file
639635
// with the `.cpp` file.
640636
let buildSettings = FileBuildSettings(
641-
compilerArguments: try await compilerArguments(
642-
for: DocumentURI(substituteFile),
643-
in: swiftPMTarget,
644-
language: request.language
645-
),
637+
compilerArguments: try await compilerArguments(for: DocumentURI(substituteFile), in: swiftPMTarget),
646638
workingDirectory: try projectRoot.filePath,
647639
language: request.language
648640
).patching(newFile: DocumentURI(try path.asURL.realpath), originalFile: DocumentURI(substituteFile))
@@ -653,11 +645,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem {
653645
}
654646

655647
return TextDocumentSourceKitOptionsResponse(
656-
compilerArguments: try await compilerArguments(
657-
for: request.textDocument.uri,
658-
in: swiftPMTarget,
659-
language: request.language
660-
),
648+
compilerArguments: try await compilerArguments(for: request.textDocument.uri, in: swiftPMTarget),
661649
workingDirectory: try projectRoot.filePath
662650
)
663651
}

Sources/SemanticIndex/CheckedIndex.swift

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ package enum IndexCheckLevel {
5858
/// - SeeAlso: Comment on `IndexOutOfDateChecker`
5959
package final class CheckedIndex {
6060
private var checker: IndexOutOfDateChecker
61-
private let index: IndexStoreDB
61+
package let unchecked: UncheckedIndex
62+
private var index: IndexStoreDB { unchecked.underlyingIndexStoreDB }
6263

6364
/// Maps the USR of a symbol to its name and the name of all its containers, from outermost to innermost.
6465
///
@@ -86,15 +87,11 @@ package final class CheckedIndex {
8687
/// `containerNamesCache[<usr of Foo>]` will be `["Bar", "Foo"]`.
8788
private var containerNamesCache: [String: [String]] = [:]
8889

89-
fileprivate init(index: IndexStoreDB, checkLevel: IndexCheckLevel) {
90-
self.index = index
90+
fileprivate init(unchecked: UncheckedIndex, checkLevel: IndexCheckLevel) {
91+
self.unchecked = unchecked
9192
self.checker = IndexOutOfDateChecker(checkLevel: checkLevel)
9293
}
9394

94-
package var unchecked: UncheckedIndex {
95-
return UncheckedIndex(index)
96-
}
97-
9895
@discardableResult
9996
package func forEachSymbolOccurrence(
10097
byUSR usr: String,
@@ -294,26 +291,43 @@ package final class CheckedIndex {
294291
/// access of the underlying `IndexStoreDB`. This makes sure that accesses to the raw `IndexStoreDB` are explicit (by
295292
/// calling `underlyingIndexStoreDB`) and we don't accidentally call into the `IndexStoreDB` when we wanted a
296293
/// `CheckedIndex`.
297-
package struct UncheckedIndex: Sendable {
298-
package let underlyingIndexStoreDB: IndexStoreDB
294+
package final actor UncheckedIndex: Sendable {
295+
package nonisolated let underlyingIndexStoreDB: IndexStoreDB
296+
297+
/// Whether the underlying `IndexStoreDB` uses has `useExplicitOutputUnits` enabled and thus needs to receive updates
298+
/// updates as output paths are added or removed from the project.
299+
package let usesExplicitOutputPaths: Bool
299300

300-
package init?(_ index: IndexStoreDB?) {
301+
/// The set of unit output paths that are currently registered in the underlying `IndexStoreDB`.
302+
private var unitOutputPaths: Set<String> = []
303+
304+
package init?(_ index: IndexStoreDB?, usesExplicitOutputPaths: Bool) {
301305
guard let index else {
302306
return nil
303307
}
308+
self.usesExplicitOutputPaths = usesExplicitOutputPaths
304309
self.underlyingIndexStoreDB = index
305310
}
306311

307-
package init(_ index: IndexStoreDB) {
308-
self.underlyingIndexStoreDB = index
312+
/// Update the set of output paths that should be considered visible in the project. For example, if a source file is
313+
/// removed from all targets in the project but remains on disk, this allows the index to start excluding it.
314+
package func setUnitOutputPaths(_ paths: Set<String>) {
315+
guard usesExplicitOutputPaths else {
316+
return
317+
}
318+
let addedPaths = paths.filter { !unitOutputPaths.contains($0) }
319+
let removedPaths = unitOutputPaths.filter { !paths.contains($0) }
320+
underlyingIndexStoreDB.addUnitOutFilePaths(Array(addedPaths), waitForProcessing: false)
321+
underlyingIndexStoreDB.removeUnitOutFilePaths(Array(removedPaths), waitForProcessing: false)
322+
self.unitOutputPaths = paths
309323
}
310324

311-
package func checked(for checkLevel: IndexCheckLevel) -> CheckedIndex {
312-
return CheckedIndex(index: underlyingIndexStoreDB, checkLevel: checkLevel)
325+
package nonisolated func checked(for checkLevel: IndexCheckLevel) -> CheckedIndex {
326+
return CheckedIndex(unchecked: self, checkLevel: checkLevel)
313327
}
314328

315329
/// Wait for IndexStoreDB to be updated based on new unit files written to disk.
316-
package func pollForUnitChangesAndWait() {
330+
package nonisolated func pollForUnitChangesAndWait() {
317331
self.underlyingIndexStoreDB.pollForUnitChangesAndWait()
318332
}
319333
}

Sources/SemanticIndex/IndexHooks.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ package protocol IndexInjector: Sendable {
2828
delegate: IndexDelegate,
2929
prefixMappings: [PathMapping]
3030
) async throws -> IndexStoreDB
31+
32+
/// Whether the injected index uses explicit output paths and whether SourceKit-LSP should thus set the unit output
33+
/// paths on the index as they change.
34+
var usesExplicitOutputPaths: Bool { get async }
3135
}
3236

3337
/// Callbacks that allow inspection of internal state modifications during testing.

Sources/SourceKitLSP/Workspace.swift

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,6 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
285285
"Created workspace at \(rootUri.forLogging) with project root \(buildSystemSpec?.projectRoot.description ?? "<nil>")"
286286
)
287287

288-
var index: IndexStoreDB? = nil
289288
var indexDelegate: SourceKitIndexDelegate? = nil
290289

291290
let indexOptions = options.indexOrDefault
@@ -301,36 +300,43 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
301300
} else {
302301
nil
303302
}
303+
let supportsOutputPaths = await buildSystemManager.initializationData?.outputPathsProvider ?? false
304+
var index: UncheckedIndex? = nil
304305
if let indexStorePath, let indexDatabasePath, let libPath = await toolchainRegistry.default?.libIndexStore {
305306
do {
306307
indexDelegate = SourceKitIndexDelegate()
307308
let prefixMappings =
308309
(indexOptions.indexPrefixMap ?? [:])
309310
.map { PathMapping(original: $0.key, replacement: $0.value) }
310311
if let indexInjector = hooks.indexHooks.indexInjector {
311-
index = try await indexInjector.createIndex(
312+
let indexStoreDB = try await indexInjector.createIndex(
312313
storePath: indexStorePath,
313314
databasePath: indexDatabasePath,
314315
indexStoreLibraryPath: libPath,
315316
delegate: indexDelegate!,
316317
prefixMappings: prefixMappings
317318
)
319+
index = UncheckedIndex(indexStoreDB, usesExplicitOutputPaths: await indexInjector.usesExplicitOutputPaths)
318320
} else {
319-
index = try IndexStoreDB(
321+
let indexStoreDB = try IndexStoreDB(
320322
storePath: indexStorePath.filePath,
321323
databasePath: indexDatabasePath.filePath,
322324
library: IndexStoreLibrary(dylibPath: libPath.filePath),
323325
delegate: indexDelegate,
326+
useExplicitOutputUnits: supportsOutputPaths,
324327
prefixMappings: prefixMappings
325328
)
326-
logger.debug("Opened IndexStoreDB at \(indexDatabasePath) with store path \(indexStorePath)")
329+
index = UncheckedIndex(indexStoreDB, usesExplicitOutputPaths: supportsOutputPaths)
330+
logger.debug(
331+
"Opened IndexStoreDB at \(indexDatabasePath) with store path \(indexStorePath) with explicit output files \(supportsOutputPaths)"
332+
)
327333
}
328334
} catch {
329335
logger.error("Failed to open IndexStoreDB: \(error.localizedDescription)")
330336
}
331337
}
332338

333-
await buildSystemManager.setMainFilesProvider(UncheckedIndex(index))
339+
await buildSystemManager.setMainFilesProvider(index)
334340

335341
await self.init(
336342
sourceKitLSPServer: sourceKitLSPServer,
@@ -339,7 +345,7 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
339345
options: options,
340346
hooks: hooks,
341347
buildSystemManager: buildSystemManager,
342-
index: UncheckedIndex(index),
348+
index: index,
343349
indexDelegate: indexDelegate,
344350
indexTaskScheduler: indexTaskScheduler
345351
)
@@ -450,6 +456,18 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
450456
let testFiles = try await buildSystemManager.testFiles()
451457
await syntacticTestIndex.listOfTestFilesDidChange(testFiles)
452458
}
459+
460+
if await self.uncheckedIndex?.usesExplicitOutputPaths ?? false {
461+
if await buildSystemManager.initializationData?.outputPathsProvider ?? false {
462+
await orLog("Setting new list of unit output paths") {
463+
try await self.uncheckedIndex?.setUnitOutputPaths(Set(buildSystemManager.outputPathInAllTargets()))
464+
}
465+
} else {
466+
// This can only happen if an index got injected that uses explicit output paths but the build system does not
467+
// support output paths.
468+
logger.error("The index uses explicit output paths but the build system does not support output paths")
469+
}
470+
}
453471
}
454472

455473
package var clientSupportsWorkDoneProgress: Bool {

Tests/SourceKitLSPTests/WorkspaceSymbolsTests.swift

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,82 @@ class WorkspaceSymbolsTests: XCTestCase {
117117
]
118118
)
119119
}
120+
121+
func testHideSymbolsFromExcludedFiles() async throws {
122+
let project = try await SwiftPMTestProject(
123+
files: [
124+
"FileA.swift": "func 1️⃣doThingA() {}",
125+
"FileB.swift": "func 2️⃣doThingB() {}",
126+
],
127+
manifest: """
128+
// swift-tools-version: 5.7
129+
130+
import PackageDescription
131+
132+
let package = Package(
133+
name: "MyLibrary",
134+
targets: [.target(name: "MyLibrary")]
135+
)
136+
""",
137+
enableBackgroundIndexing: true
138+
)
139+
let symbolsBeforeDeletion = try await project.testClient.send(WorkspaceSymbolsRequest(query: "doThing"))
140+
XCTAssertEqual(
141+
symbolsBeforeDeletion,
142+
[
143+
.symbolInformation(
144+
SymbolInformation(
145+
name: "doThingA()",
146+
kind: .function,
147+
location: try project.location(from: "1️⃣", to: "1️⃣", in: "FileA.swift")
148+
)
149+
),
150+
.symbolInformation(
151+
SymbolInformation(
152+
name: "doThingB()",
153+
kind: .function,
154+
location: try project.location(from: "2️⃣", to: "2️⃣", in: "FileB.swift")
155+
)
156+
),
157+
]
158+
)
159+
160+
try """
161+
// swift-tools-version: 5.7
162+
163+
import PackageDescription
164+
165+
let package = Package(
166+
name: "MyLibrary",
167+
targets: [.target(name: "MyLibrary", exclude: ["FileA.swift"])]
168+
)
169+
""".write(to: XCTUnwrap(project.uri(for: "Package.swift").fileURL), atomically: true, encoding: .utf8)
170+
171+
// Test that we exclude FileB.swift from the index after it has been removed from the package's target.
172+
project.testClient.send(
173+
DidChangeWatchedFilesNotification(
174+
changes: [FileEvent(uri: try project.uri(for: "Package.swift"), type: .changed)]
175+
)
176+
)
177+
try await repeatUntilExpectedResult {
178+
let symbolsAfterDeletion = try await project.testClient.send(WorkspaceSymbolsRequest(query: "doThing"))
179+
if symbolsAfterDeletion?.count == 2 {
180+
// The exclusion hasn't been processed yet, try again.
181+
return false
182+
}
183+
XCTAssertEqual(
184+
symbolsAfterDeletion,
185+
[
186+
.symbolInformation(
187+
SymbolInformation(
188+
name: "doThingB()",
189+
kind: .function,
190+
location: try project.location(from: "2️⃣", to: "2️⃣", in: "FileB.swift")
191+
)
192+
)
193+
]
194+
)
195+
return true
196+
}
197+
}
120198
}

0 commit comments

Comments
 (0)