Skip to content

Commit b6a2008

Browse files
committed
Merge branch 'main' into 6.0/merge-main-2024-04-23
2 parents 10ddcb5 + ec5c614 commit b6a2008

36 files changed

+1992
-139
lines changed

Sources/Diagnose/DiagnoseCommand.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ private var progressBar: PercentProgressAnimation? = nil
2727

2828
/// A component of the diagnostic bundle that's collected in independent stages.
2929
fileprivate enum BundleComponent: String, CaseIterable, ExpressibleByArgument {
30-
case crashReports
31-
case logs
32-
case swiftVersions
33-
case sourcekitdCrashes
34-
case swiftFrontendCrashes
30+
case crashReports = "crash-reports"
31+
case logs = "logs"
32+
case swiftVersions = "swift-versions"
33+
case sourcekitdCrashes = "sourcekitd-crashes"
34+
case swiftFrontendCrashes = "swift-frontend-crashes"
3535
}
3636

3737
public struct DiagnoseCommand: AsyncParsableCommand {

Sources/Diagnose/ReproducerBundle.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func makeReproducerBundle(for requestInfo: RequestInfo, toolchain: Toolchain, bu
4141
} else {
4242
let request = try requestInfo.request(for: URL(fileURLWithPath: "/input.swift"))
4343
try request.write(
44-
to: bundlePath.appendingPathComponent("request.json"),
44+
to: bundlePath.appendingPathComponent("request.yml"),
4545
atomically: true,
4646
encoding: .utf8
4747
)

Sources/SKCore/BuildServerBuildSystem.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,17 @@ extension BuildServerBuildSystem: BuildSystem {
316316

317317
return .unhandled
318318
}
319+
320+
public func testFiles() async -> [DocumentURI] {
321+
// BuildServerBuildSystem does not support syntactic test discovery
322+
// (https://github.com/apple/sourcekit-lsp/issues/1173).
323+
return []
324+
}
325+
326+
public func addTestFilesDidChangeCallback(_ callback: @escaping () async -> Void) {
327+
// BuildServerBuildSystem does not support syntactic test discovery
328+
// (https://github.com/apple/sourcekit-lsp/issues/1173).
329+
}
319330
}
320331

321332
private func loadBuildServerConfig(path: AbsolutePath, fileSystem: FileSystem) throws -> BuildServerConfig {

Sources/SKCore/BuildSystem.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ public protocol BuildSystem: AnyObject, Sendable {
8787
func filesDidChange(_ events: [FileEvent]) async
8888

8989
func fileHandlingCapability(for uri: DocumentURI) async -> FileHandlingCapability
90+
91+
/// Returns the list of files that might contain test cases.
92+
///
93+
/// The returned file list is an over-approximation. It might contain tests from non-test targets or files that don't
94+
/// actually contain any tests. Keeping this list as minimal as possible helps reduce the amount of work that the
95+
/// syntactic test indexer needs to perform.
96+
func testFiles() async -> [DocumentURI]
97+
98+
/// Adds a callback that should be called when the value returned by `testFiles()` changes.
99+
///
100+
/// The callback might also be called without an actual change to `testFiles`.
101+
func addTestFilesDidChangeCallback(_ callback: @Sendable @escaping () async -> Void) async
90102
}
91103

92104
public let buildTargetsNotSupported =

Sources/SKCore/BuildSystemManager.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ extension BuildSystemManager {
176176
fallbackBuildSystem != nil ? .fallback : .unhandled
177177
)
178178
}
179+
180+
public func testFiles() async -> [DocumentURI] {
181+
return await buildSystem?.testFiles() ?? []
182+
}
179183
}
180184

181185
extension BuildSystemManager: BuildSystemDelegate {

Sources/SKCore/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ add_library(SKCore STATIC
77
BuildSystemManager.swift
88
CompilationDatabase.swift
99
CompilationDatabaseBuildSystem.swift
10+
Debouncer.swift
1011
FallbackBuildSystem.swift
1112
FileBuildSettings.swift
1213
MainFilesProvider.swift

Sources/SKCore/CompilationDatabaseBuildSystem.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ public actor CompilationDatabaseBuildSystem {
3939
/// Delegate to handle any build system events.
4040
public weak var delegate: BuildSystemDelegate? = nil
4141

42+
/// Callbacks that should be called if the list of possible test files has changed.
43+
public var testFilesDidChangeCallbacks: [() async -> Void] = []
44+
4245
public func setDelegate(_ delegate: BuildSystemDelegate?) async {
4346
self.delegate = delegate
4447
}
@@ -167,6 +170,9 @@ extension CompilationDatabaseBuildSystem: BuildSystem {
167170
if let delegate = self.delegate {
168171
await delegate.fileBuildSettingsChanged(self.watchedFiles)
169172
}
173+
for testFilesDidChangeCallback in testFilesDidChangeCallbacks {
174+
await testFilesDidChangeCallback()
175+
}
170176
}
171177

172178
public func filesDidChange(_ events: [FileEvent]) async {
@@ -185,4 +191,12 @@ extension CompilationDatabaseBuildSystem: BuildSystem {
185191
return .unhandled
186192
}
187193
}
194+
195+
public func testFiles() async -> [DocumentURI] {
196+
return compdb?.allCommands.map { DocumentURI($0.url) } ?? []
197+
}
198+
199+
public func addTestFilesDidChangeCallback(_ callback: @escaping () async -> Void) async {
200+
testFilesDidChangeCallbacks.append(callback)
201+
}
188202
}

Sources/SKCore/Debouncer.swift

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// Debounces calls to a function/closure. If multiple calls to the closure are made, it allows aggregating the
14+
/// parameters.
15+
public actor Debouncer<Parameter> {
16+
/// How long to wait for further `scheduleCall` calls before committing to actually calling `makeCall`.
17+
private let debounceDuration: Duration
18+
19+
/// When `scheduleCall` is called while another `scheduleCall` was waiting to commit its call, combines the parameters
20+
/// of those two calls.
21+
///
22+
/// ### Example
23+
///
24+
/// Two `scheduleCall` calls that are made within a time period shorter than `debounceDuration` like the following
25+
/// ```swift
26+
/// debouncer.scheduleCall(5)
27+
/// debouncer.scheduleCall(10)
28+
/// ```
29+
/// will call `combineParameters(5, 10)`
30+
private let combineParameters: (Parameter, Parameter) -> Parameter
31+
32+
/// After the debounce duration has elapsed, commit the call.
33+
private let makeCall: (Parameter) async -> Void
34+
35+
/// In the time between the call to `scheduleCall` and the call actually being committed (ie. in the time that the
36+
/// call can be debounced), the task that would commit the call (unless cancelled) and the parameter with which this
37+
/// call should be made.
38+
private var inProgressData: (Parameter, Task<Void, Never>)?
39+
40+
public init(
41+
debounceDuration: Duration,
42+
combineResults: @escaping (Parameter, Parameter) -> Parameter,
43+
_ makeCall: @escaping (Parameter) async -> Void
44+
) {
45+
self.debounceDuration = debounceDuration
46+
self.combineParameters = combineResults
47+
self.makeCall = makeCall
48+
}
49+
50+
/// Schedule a debounced call. If `scheduleCall` is called within `debounceDuration`, the parameters of the two
51+
/// `scheduleCall` calls will be combined using `combineParameters` and the new debounced call will be scheduled
52+
/// `debounceDuration` after the second `scheduleCall` call.
53+
public func scheduleCall(_ parameter: Parameter) {
54+
var parameter = parameter
55+
if let (inProgressParameter, inProgressTask) = inProgressData {
56+
inProgressTask.cancel()
57+
parameter = combineParameters(inProgressParameter, parameter)
58+
}
59+
let task = Task {
60+
do {
61+
try await Task.sleep(for: debounceDuration)
62+
try Task.checkCancellation()
63+
} catch {
64+
return
65+
}
66+
inProgressData = nil
67+
await makeCall(parameter)
68+
}
69+
inProgressData = (parameter, task)
70+
}
71+
}
72+
73+
extension Debouncer<Void> {
74+
public init(debounceDuration: Duration, _ makeCall: @escaping () async -> Void) {
75+
self.init(debounceDuration: debounceDuration, combineResults: { _, _ in }, makeCall)
76+
}
77+
78+
public func scheduleCall() {
79+
self.scheduleCall(())
80+
}
81+
}

Sources/SKSupport/AsyncQueue.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import Foundation
1616
/// array.
1717
private protocol AnyTask: Sendable {
1818
func waitForCompletion() async
19+
20+
func cancel()
1921
}
2022

2123
extension Task: AnyTask {
@@ -89,6 +91,16 @@ public final class AsyncQueue<TaskMetadata: DependencyTracker>: Sendable {
8991

9092
public init() {}
9193

94+
public func cancelTasks(where filter: (TaskMetadata) -> Bool) {
95+
pendingTasks.withLock { pendingTasks in
96+
for task in pendingTasks {
97+
if filter(task.metadata) {
98+
task.task.cancel()
99+
}
100+
}
101+
}
102+
}
103+
92104
/// Schedule a new closure to be executed on the queue.
93105
///
94106
/// If this is a serial queue, all previously added tasks are guaranteed to

Sources/SKSwiftPMWorkspace/SwiftPMBuildSystem.swift

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ public actor SwiftPMBuildSystem {
8181
self.delegate = delegate
8282
}
8383

84+
/// Callbacks that should be called if the list of possible test files has changed.
85+
public var testFilesDidChangeCallbacks: [() async -> Void] = []
86+
8487
let workspacePath: TSCAbsolutePath
8588
/// The directory containing `Package.swift`.
8689
public var projectRoot: TSCAbsolutePath
@@ -99,6 +102,16 @@ public actor SwiftPMBuildSystem {
99102
/// This callback is informed when `reloadPackage` starts and ends executing.
100103
var reloadPackageStatusCallback: (ReloadPackageStatus) async -> Void
101104

105+
/// Debounces calls to `delegate.filesDependenciesUpdated`.
106+
///
107+
/// This is to ensure we don't call `filesDependenciesUpdated` for the same file multiple time if the client does not
108+
/// debounce `workspace/didChangeWatchedFiles` and sends a separate notification eg. for every file within a target as
109+
/// it's being updated by a git checkout, which would cause other files within that target to receive a
110+
/// `fileDependenciesUpdated` call once for every updated file within the target.
111+
///
112+
/// Force-unwrapped optional because initializing it requires access to `self`.
113+
var fileDependenciesUpdatedDebouncer: Debouncer<Set<DocumentURI>>! = nil
114+
102115
/// Creates a build system using the Swift Package Manager, if this workspace is a package.
103116
///
104117
/// - Parameters:
@@ -166,6 +179,19 @@ public actor SwiftPMBuildSystem {
166179
self.modulesGraph = try ModulesGraph(rootPackages: [], dependencies: [], binaryArtifacts: [:])
167180
self.reloadPackageStatusCallback = reloadPackageStatusCallback
168181

182+
// The debounce duration of 500ms was chosen arbitrarily without scientific research.
183+
self.fileDependenciesUpdatedDebouncer = Debouncer(
184+
debounceDuration: .milliseconds(500),
185+
combineResults: { $0.union($1) }
186+
) {
187+
[weak self] (filesWithUpdatedDependencies) in
188+
guard let delegate = await self?.delegate else {
189+
logger.fault("Not calling filesDependenciesUpdated because no delegate exists in SwiftPMBuildSystem")
190+
return
191+
}
192+
await delegate.filesDependenciesUpdated(filesWithUpdatedDependencies)
193+
}
194+
169195
try await reloadPackage()
170196
}
171197

@@ -267,6 +293,9 @@ extension SwiftPMBuildSystem {
267293
}
268294
await delegate.fileBuildSettingsChanged(self.watchedFiles)
269295
await delegate.fileHandlingCapabilityChanged()
296+
for testFilesDidChangeCallback in testFilesDidChangeCallbacks {
297+
await testFilesDidChangeCallback()
298+
}
270299
}
271300
}
272301

@@ -368,6 +397,34 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
368397
try await self.reloadPackage()
369398
}
370399
}
400+
401+
var filesWithUpdatedDependencies: Set<DocumentURI> = []
402+
// If a Swift file within a target is updated, reload all the other files within the target since they might be
403+
// referring to a function in the updated file.
404+
for event in events {
405+
guard let url = event.uri.fileURL,
406+
url.pathExtension == "swift",
407+
let absolutePath = try? AbsolutePath(validating: url.path),
408+
let target = fileToTarget[absolutePath]
409+
else {
410+
continue
411+
}
412+
filesWithUpdatedDependencies.formUnion(target.sources.map { DocumentURI($0) })
413+
}
414+
415+
// If a `.swiftmodule` file is updated, this means that we have performed a build / are
416+
// performing a build and files that depend on this module have updated dependencies.
417+
// We don't have access to the build graph from the SwiftPM API offered to SourceKit-LSP to figure out which files
418+
// depend on the updated module, so assume that all files have updated dependencies.
419+
// The file watching here is somewhat fragile as well because it assumes that the `.swiftmodule` files are being
420+
// written to a directory within the workspace root. This is not necessarily true if the user specifies a build
421+
// directory outside the source tree.
422+
// All of this shouldn't be necessary once we have background preparation, in which case we know when preparation of
423+
// a target has finished.
424+
if events.contains(where: { $0.uri.fileURL?.pathExtension == "swiftmodule" }) {
425+
filesWithUpdatedDependencies.formUnion(self.fileToTarget.keys.map { DocumentURI($0.asURL) })
426+
}
427+
await self.fileDependenciesUpdatedDebouncer.scheduleCall(filesWithUpdatedDependencies)
371428
}
372429

373430
public func fileHandlingCapability(for uri: DocumentURI) -> FileHandlingCapability {
@@ -380,6 +437,15 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
380437
return .unhandled
381438
}
382439
}
440+
441+
public func testFiles() -> [DocumentURI] {
442+
// We should only include source files from test targets (https://github.com/apple/sourcekit-lsp/issues/1174).
443+
return fileToTarget.map { DocumentURI($0.key.asURL) }
444+
}
445+
446+
public func addTestFilesDidChangeCallback(_ callback: @Sendable @escaping () async -> Void) async {
447+
testFilesDidChangeCallbacks.append(callback)
448+
}
383449
}
384450

385451
extension SwiftPMBuildSystem {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
15+
extension FileManager {
16+
/// Returns the URLs of all files with the given file extension in the given directory (recursively).
17+
public func findFiles(withExtension extensionName: String, in directory: URL) -> [URL] {
18+
var result: [URL] = []
19+
let enumerator = self.enumerator(at: directory, includingPropertiesForKeys: nil)
20+
while let url = enumerator?.nextObject() as? URL {
21+
if url.pathExtension == extensionName {
22+
result.append(url)
23+
}
24+
}
25+
return result
26+
}
27+
}

Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,25 @@ public struct IndexedSingleSwiftFileTestProject {
6868

6969
if let sdk = TibsBuilder.defaultSDKPath {
7070
compilerArguments += ["-sdk", sdk]
71+
72+
// The following are needed so we can import XCTest
73+
let sdkUrl = URL(fileURLWithPath: sdk)
74+
let usrLibDir =
75+
sdkUrl
76+
.deletingLastPathComponent()
77+
.deletingLastPathComponent()
78+
.appendingPathComponent("usr")
79+
.appendingPathComponent("lib")
80+
let frameworksDir =
81+
sdkUrl
82+
.deletingLastPathComponent()
83+
.deletingLastPathComponent()
84+
.appendingPathComponent("Library")
85+
.appendingPathComponent("Frameworks")
86+
compilerArguments += [
87+
"-I", usrLibDir.path,
88+
"-F", frameworksDir.path,
89+
]
7190
}
7291

7392
let compilationDatabase = JSONCompilationDatabase(

Sources/SKTestSupport/MultiFileTestProject.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,13 @@ public class MultiFileTestProject {
142142
}
143143
return DocumentPositions(markedText: fileData.markedText)[marker]
144144
}
145+
146+
public func range(from fromMarker: String, to toMarker: String, in fileName: String) throws -> Range<Position> {
147+
return try position(of: fromMarker, in: fileName)..<position(of: toMarker, in: fileName)
148+
}
149+
150+
public func location(from fromMarker: String, to toMarker: String, in fileName: String) throws -> Location {
151+
let range = try self.range(from: fromMarker, to: toMarker, in: fileName)
152+
return Location(uri: try self.uri(for: fileName), range: range)
153+
}
145154
}

0 commit comments

Comments
 (0)