Use fallback build settings if build system doesn’t provide build settings within a timeout

When we receive build settings after hitting the timeout, we call `fileBuildSettingsChanged` on the delegate, which should cause the document to get re-opened in sourcekitd and diagnostics to get refreshed.

rdar://136332685
Fixes #1693
This commit is contained in:
Alex Hoppen
2024-09-20 17:11:07 -07:00
parent edafb120ee
commit 5bae73fca8
26 changed files with 346 additions and 80 deletions

View File

@@ -31,6 +31,7 @@ The structure of the file is currently not guaranteed to be stable. Options may
- `cxxCompilerFlags: string[]`: Extra arguments passed to the compiler for C++ files
- `swiftCompilerFlags: string[]`: Extra arguments passed to the compiler for Swift files
- `sdk: string`: The SDK to use for fallback arguments. Default is to infer the SDK using `xcrun`.
- `buildSettingsTimeout: int`: Number of milliseconds to wait for build settings from the build system before using fallback build settings.
- `clangdOptions: string[]`: Extra command line arguments passed to `clangd` when launching it
- `index`: Dictionary with the following keys, defining options related to indexing
- `indexStorePath: string`: Directory in which a separate compilation stores the index store. By default, inferred from the build system.

View File

@@ -139,6 +139,7 @@ private extension BuildSystemKind {
private static func createBuiltInBuildSystemAdapter(
projectRoot: AbsolutePath,
messagesToSourceKitLSPHandler: any MessageHandler,
buildSystemTestHooks: BuildSystemTestHooks,
_ createBuildSystem: @Sendable (_ connectionToSourceKitLSP: any Connection) async throws -> BuiltInBuildSystem?
) async -> BuildSystemAdapter? {
let connectionToSourceKitLSP = LocalConnection(
@@ -156,7 +157,8 @@ private extension BuildSystemKind {
logger.log("Created \(type(of: buildSystem), privacy: .public) at \(projectRoot.pathString)")
let buildSystemAdapter = BuiltInBuildSystemAdapter(
underlyingBuildSystem: buildSystem,
connectionToSourceKitLSP: connectionToSourceKitLSP
connectionToSourceKitLSP: connectionToSourceKitLSP,
buildSystemTestHooks: buildSystemTestHooks
)
let connectionToBuildSystem = LocalConnection(
receiverName: "\(type(of: buildSystem)) for \(projectRoot.asURL.lastPathComponent)"
@@ -190,7 +192,8 @@ private extension BuildSystemKind {
case .compilationDatabase(projectRoot: let projectRoot):
return await Self.createBuiltInBuildSystemAdapter(
projectRoot: projectRoot,
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler,
buildSystemTestHooks: testHooks
) { connectionToSourceKitLSP in
CompilationDatabaseBuildSystem(
projectRoot: projectRoot,
@@ -203,7 +206,8 @@ private extension BuildSystemKind {
case .swiftPM(projectRoot: let projectRoot):
return await Self.createBuiltInBuildSystemAdapter(
projectRoot: projectRoot,
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler,
buildSystemTestHooks: testHooks
) { connectionToSourceKitLSP in
try await SwiftPMBuildSystem(
projectRoot: projectRoot,
@@ -216,7 +220,8 @@ private extension BuildSystemKind {
case .testBuildSystem(projectRoot: let projectRoot):
return await Self.createBuiltInBuildSystemAdapter(
projectRoot: projectRoot,
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler,
buildSystemTestHooks: testHooks
) { connectionToSourceKitLSP in
TestBuildSystem(projectRoot: projectRoot, connectionToSourceKitLSP: connectionToSourceKitLSP)
}
@@ -411,7 +416,8 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
)
let adapter = BuiltInBuildSystemAdapter(
underlyingBuildSystem: legacyBuildServer,
connectionToSourceKitLSP: legacyBuildServer.connectionToSourceKitLSP
connectionToSourceKitLSP: legacyBuildServer.connectionToSourceKitLSP,
buildSystemTestHooks: buildSystemTestHooks
)
let connectionToBuildSystem = LocalConnection(receiverName: "Legacy BSP server")
connectionToBuildSystem.start(handler: adapter)
@@ -672,7 +678,12 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
/// Returns the target's module name as parsed from the `BuildTargetIdentifier`'s compiler arguments.
package func moduleName(for document: DocumentURI, in target: BuildTargetIdentifier) async -> String? {
guard let language = await self.defaultLanguage(for: document, in: target),
let buildSettings = await buildSettings(for: document, in: target, language: language)
let buildSettings = await buildSettings(
for: document,
in: target,
language: language,
fallbackAfterTimeout: false
)
else {
return nil
}
@@ -715,11 +726,6 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
language: language
)
// TODO: We should only wait `fallbackSettingsTimeout` for build settings
// and return fallback afterwards.
// For now, this should be fine because all build systems return
// very quickly from `settings(for:language:)`.
// https://github.com/apple/sourcekit-lsp/issues/1181
let response = try await cachedSourceKitOptions.get(request, isolation: self) { request in
try await buildSystemAdapter.send(request)
}
@@ -740,19 +746,30 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
/// Only call this method if it is known that `document` is a main file. Prefer `buildSettingsInferredFromMainFile`
/// otherwise. If `document` is a header file, this will most likely return fallback settings because header files
/// don't have build settings by themselves.
///
/// If `fallbackAfterTimeout` is true fallback build settings will be returned if no build settings can be found in
/// `SourceKitLSPOptions.buildSettingsTimeoutOrDefault`.
package func buildSettings(
for document: DocumentURI,
in target: BuildTargetIdentifier?,
language: Language
language: Language,
fallbackAfterTimeout: Bool
) async -> FileBuildSettings? {
do {
if let target,
let buildSettings = try await buildSettingsFromBuildSystem(for: document, in: target, language: language)
{
return buildSettings
if let target {
let buildSettingsFromBuildSystem = await orLog("Getting build settings") {
if fallbackAfterTimeout {
try await withTimeout(options.buildSettingsTimeoutOrDefault) {
return try await self.buildSettingsFromBuildSystem(for: document, in: target, language: language)
} resultReceivedAfterTimeout: {
await self.delegate?.fileBuildSettingsChanged([document])
}
} else {
try await self.buildSettingsFromBuildSystem(for: document, in: target, language: language)
}
}
if let buildSettingsFromBuildSystem {
return buildSettingsFromBuildSystem
}
} catch {
logger.error("Getting build settings failed: \(error.forLogging)")
}
guard
@@ -780,14 +797,27 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
/// by the header file.
package func buildSettingsInferredFromMainFile(
for document: DocumentURI,
language: Language
language: Language,
fallbackAfterTimeout: Bool
) async -> FileBuildSettings? {
func mainFileAndSettings(
basedOn document: DocumentURI
) async -> (mainFile: DocumentURI, settings: FileBuildSettings)? {
let mainFile = await self.mainFile(for: document, language: language)
let target = await canonicalTarget(for: mainFile)
guard let settings = await buildSettings(for: mainFile, in: target, language: language) else {
let settings = await orLog("Getting build settings") {
let target = try await withTimeout(options.buildSettingsTimeoutOrDefault) {
await self.canonicalTarget(for: mainFile)
} resultReceivedAfterTimeout: {
await self.delegate?.fileBuildSettingsChanged([document])
}
return await self.buildSettings(
for: mainFile,
in: target,
language: language,
fallbackAfterTimeout: fallbackAfterTimeout
)
}
guard let settings else {
return nil
}
return (mainFile, settings)

View File

@@ -10,10 +10,25 @@
//
//===----------------------------------------------------------------------===//
#if compiler(>=6)
package import LanguageServerProtocol
#else
import LanguageServerProtocol
#endif
package struct BuildSystemTestHooks: Sendable {
package var swiftPMTestHooks: SwiftPMTestHooks
package init(swiftPMTestHooks: SwiftPMTestHooks = SwiftPMTestHooks()) {
/// A hook that will be executed before a request is handled by a `BuiltInBuildSystem`.
///
/// This allows requests to be artificially delayed.
package var handleRequest: (@Sendable (any RequestType) async -> Void)?
package init(
swiftPMTestHooks: SwiftPMTestHooks = SwiftPMTestHooks(),
handleRequest: (@Sendable (any RequestType) async -> Void)? = nil
) {
self.swiftPMTestHooks = swiftPMTestHooks
self.handleRequest = handleRequest
}
}

View File

@@ -59,6 +59,8 @@ actor BuiltInBuildSystemAdapter: QueueBasedMessageHandler {
/// The connection with which messages are sent to `BuildSystemManager`.
private let connectionToSourceKitLSP: LocalConnection
private let buildSystemTestHooks: BuildSystemTestHooks
/// If the underlying build system is a `TestBuildSystem`, return it. Otherwise, `nil`
///
/// - Important: For testing purposes only.
@@ -70,10 +72,12 @@ actor BuiltInBuildSystemAdapter: QueueBasedMessageHandler {
/// from the build system to SourceKit-LSP.
init(
underlyingBuildSystem: BuiltInBuildSystem,
connectionToSourceKitLSP: LocalConnection
connectionToSourceKitLSP: LocalConnection,
buildSystemTestHooks: BuildSystemTestHooks
) {
self.underlyingBuildSystem = underlyingBuildSystem
self.connectionToSourceKitLSP = connectionToSourceKitLSP
self.buildSystemTestHooks = buildSystemTestHooks
}
deinit {
@@ -116,6 +120,7 @@ actor BuiltInBuildSystemAdapter: QueueBasedMessageHandler {
reply: @Sendable @escaping (LSPResult<Request.Response>) -> Void
) async {
let request = RequestAndReply(request, reply: reply)
await buildSystemTestHooks.handleRequest?(request.params)
switch request {
case let request as RequestAndReply<BuildShutdownRequest>:
await request.reply { VoidResponse() }

View File

@@ -14,7 +14,6 @@ add_library(BuildSystemIntegration STATIC
ExternalBuildSystemAdapter.swift
FallbackBuildSettings.swift
FileBuildSettings.swift
Language+InferredFromFileExtension.swift
LegacyBuildServerBuildSystem.swift
MainFilesProvider.swift
PathPrefixMapping.swift

View File

@@ -250,6 +250,13 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
set { fallbackBuildSystem = newValue }
}
/// Number of milliseconds to wait for build settings from the build system before using fallback build settings.
public var buildSettingsTimeout: Int?
public var buildSettingsTimeoutOrDefault: Duration {
// The default timeout of 500ms was chosen arbitrarily without any measurements.
get { .milliseconds(buildSettingsTimeout ?? 500) }
}
public var clangdOptions: [String]?
private var index: IndexOptions?

View File

@@ -8,6 +8,7 @@ add_library(SKSupport STATIC
DocumentURI+CustomLogStringConvertible.swift
DocumentURI+symlinkTarget.swift
FileSystem.swift
Language+InferredFromFileExtension.swift
LineTable.swift
LocalConnection.swift
Process+Run.swift

View File

@@ -10,11 +10,16 @@
//
//===----------------------------------------------------------------------===//
#if compiler(>=6)
import Foundation
package import LanguageServerProtocol
#else
import Foundation
import LanguageServerProtocol
#endif
extension Language {
init?(inferredFromFileExtension uri: DocumentURI) {
package init?(inferredFromFileExtension uri: DocumentURI) {
// URL.pathExtension is only set for file URLs but we want to also infer a file extension for non-file URLs like
// untitled:file.cpp
let pathExtension = uri.fileURL?.pathExtension ?? (uri.pseudoPath as NSString).pathExtension

View File

@@ -56,12 +56,16 @@ package struct WrappedSemaphore: Sendable {
}
/// Wait for a signal and emit an XCTFail if the semaphore is not signaled within `timeout`.
package func waitOrXCTFail(timeout: DispatchTime = DispatchTime.now() + .seconds(Int(defaultTimeout))) {
package func waitOrXCTFail(
timeout: DispatchTime = DispatchTime.now() + .seconds(Int(defaultTimeout)),
file: StaticString = #filePath,
line: UInt = #line
) {
switch self.wait(timeout: timeout) {
case .success:
break
case .timedOut:
XCTFail("\(name) timed out")
XCTFail("\(name) timed out", file: file, line: line)
}
}
}

View File

@@ -254,7 +254,12 @@ package struct UpdateIndexStoreTaskDescription: IndexTaskDescription {
logger.error("Not indexing \(file.forLogging) because its language could not be determined")
return
}
let buildSettings = await buildSystemManager.buildSettings(for: file.mainFile, in: target, language: language)
let buildSettings = await buildSystemManager.buildSettings(
for: file.mainFile,
in: target,
language: language,
fallbackAfterTimeout: false
)
guard let buildSettings else {
logger.error("Not indexing \(file.forLogging) because it has no compiler arguments")
return

View File

@@ -129,14 +129,15 @@ actor ClangLanguageService: LanguageService, MessageHandler {
try startClangdProcess()
}
private func buildSettings(for document: DocumentURI) async -> ClangBuildSettings? {
private func buildSettings(for document: DocumentURI, fallbackAfterTimeout: Bool) async -> ClangBuildSettings? {
guard let workspace = workspace.value, let language = openDocuments[document] else {
return nil
}
guard
let settings = await workspace.buildSystemManager.buildSettingsInferredFromMainFile(
for: document,
language: language
language: language,
fallbackAfterTimeout: fallbackAfterTimeout
)
else {
return nil
@@ -340,7 +341,7 @@ actor ClangLanguageService: LanguageService, MessageHandler {
extension ClangLanguageService {
/// Intercept clangd's `PublishDiagnosticsNotification` to withold it if we're using fallback
/// Intercept clangd's `PublishDiagnosticsNotification` to withhold it if we're using fallback
/// build settings.
func publishDiagnostics(_ notification: PublishDiagnosticsNotification) async {
// Technically, the publish diagnostics notification could still originate
@@ -354,7 +355,9 @@ extension ClangLanguageService {
// short and we expect clangd to send us new diagnostics with the updated
// non-fallback settings very shortly after, which will override the
// incorrect result, making it very temporary.
let buildSettings = await self.buildSettings(for: notification.uri)
// TODO: We want to know the build settings that are currently transmitted to clangd, not whichever ones we would
// get next. (https://github.com/swiftlang/sourcekit-lsp/issues/1761)
let buildSettings = await self.buildSettings(for: notification.uri, fallbackAfterTimeout: true)
guard let sourceKitLSPServer else {
logger.fault("Cannot publish diagnostics because SourceKitLSPServer has been destroyed")
return
@@ -452,7 +455,7 @@ extension ClangLanguageService {
logger.error("Received updated build settings for non-file URI '\(uri.forLogging)'. Ignoring the update.")
return
}
let clangBuildSettings = await self.buildSettings(for: uri)
let clangBuildSettings = await self.buildSettings(for: uri, fallbackAfterTimeout: false)
// The compile command changed, send over the new one.
if let compileCommand = clangBuildSettings?.compileCommand,

View File

@@ -364,7 +364,8 @@ extension SwiftLanguageService {
let req = sourcekitd.dictionary([
keys.request: sourcekitd.requests.nameTranslation,
keys.sourceFile: snapshot.uri.pseudoPath,
keys.compilerArgs: await self.buildSettings(for: snapshot.uri)?.compilerArgs as [SKDRequestValue]?,
keys.compilerArgs: await self.buildSettings(for: snapshot.uri, fallbackAfterTimeout: false)?.compilerArgs
as [SKDRequestValue]?,
keys.offset: snapshot.utf8Offset(of: snapshot.position(of: symbolLocation)),
keys.nameKind: sourcekitd.values.nameSwift,
keys.baseName: name.baseName,
@@ -415,7 +416,8 @@ extension SwiftLanguageService {
let req = sourcekitd.dictionary([
keys.request: sourcekitd.requests.nameTranslation,
keys.sourceFile: snapshot.uri.pseudoPath,
keys.compilerArgs: await self.buildSettings(for: snapshot.uri)?.compilerArgs as [SKDRequestValue]?,
keys.compilerArgs: await self.buildSettings(for: snapshot.uri, fallbackAfterTimeout: false)?.compilerArgs
as [SKDRequestValue]?,
keys.offset: snapshot.utf8Offset(of: snapshot.position(of: symbolLocation)),
keys.nameKind: sourcekitd.values.nameObjc,
])

View File

@@ -34,7 +34,7 @@ extension SwiftLanguageService {
let clientSupportsSnippets =
capabilityRegistry.clientCapabilities.textDocument?.completion?.completionItem?.snippetSupport ?? false
let buildSettings = await buildSettings(for: snapshot.uri)
let buildSettings = await buildSettings(for: snapshot.uri, fallbackAfterTimeout: false)
let inferredIndentationWidth = BasicFormat.inferIndentation(of: await syntaxTreeManager.syntaxTree(for: snapshot))

View File

@@ -141,10 +141,12 @@ extension SwiftLanguageService {
/// - Parameters:
/// - url: Document URI in which to perform the request. Must be an open document.
/// - range: The position range within the document to lookup the symbol at.
/// - completion: Completion block to asynchronously receive the CursorInfo, or error.
/// - fallbackSettingsAfterTimeout: Whether fallback build settings should be used for the cursor info request if no
/// build settings can be retrieved within a timeout.
func cursorInfo(
_ uri: DocumentURI,
_ range: Range<Position>,
fallbackSettingsAfterTimeout: Bool,
additionalParameters appendAdditionalParameters: ((SKDRequestDictionary) -> Void)? = nil
) async throws -> (cursorInfo: [CursorInfo], refactorActions: [SemanticRefactorCommand]) {
let documentManager = try self.documentManager
@@ -161,7 +163,8 @@ extension SwiftLanguageService {
keys.length: offsetRange.upperBound != offsetRange.lowerBound ? offsetRange.count : nil,
keys.sourceFile: snapshot.uri.sourcekitdSourceFile,
keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath,
keys.compilerArgs: await self.buildSettings(for: uri)?.compilerArgs as [SKDRequestValue]?,
keys.compilerArgs: await self.buildSettings(for: uri, fallbackAfterTimeout: fallbackSettingsAfterTimeout)?
.compilerArgs as [SKDRequestValue]?,
])
appendAdditionalParameters?(skreq)

View File

@@ -86,7 +86,7 @@ actor MacroExpansionManager {
}
let snapshot = try await swiftLanguageService.latestSnapshot(for: uri)
let buildSettings = await swiftLanguageService.buildSettings(for: uri)
let buildSettings = await swiftLanguageService.buildSettings(for: uri, fallbackAfterTimeout: false)
if let cacheEntry = cache.first(where: {
$0.snapshotID == snapshot.id && $0.range == range && $0.buildSettings == buildSettings

View File

@@ -100,7 +100,8 @@ extension SwiftLanguageService {
keys.groupName: groupName,
keys.name: interfaceURI.pseudoPath,
keys.synthesizedExtension: 1,
keys.compilerArgs: await self.buildSettings(for: document)?.compilerArgs as [SKDRequestValue]?,
keys.compilerArgs: await self.buildSettings(for: document, fallbackAfterTimeout: false)?.compilerArgs
as [SKDRequestValue]?,
])
let dict = try await sendSourcekitdRequest(skreq, fileContents: nil)

View File

@@ -126,7 +126,8 @@ extension SwiftLanguageService {
keys.column: utf8Column + 1,
keys.length: snapshot.utf8OffsetRange(of: refactorCommand.positionRange).count,
keys.actionUID: self.sourcekitd.api.uid_get_from_cstr(refactorCommand.actionString)!,
keys.compilerArgs: await self.buildSettings(for: snapshot.uri)?.compilerArgs as [SKDRequestValue]?,
keys.compilerArgs: await self.buildSettings(for: snapshot.uri, fallbackAfterTimeout: true)?.compilerArgs
as [SKDRequestValue]?,
])
let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text)

View File

@@ -73,7 +73,8 @@ extension SwiftLanguageService {
keys.sourceFile: snapshot.uri.sourcekitdSourceFile,
keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath,
keys.includeNonEditableBaseNames: includeNonEditableBaseNames ? 1 : 0,
keys.compilerArgs: await self.buildSettings(for: snapshot.uri)?.compilerArgs as [SKDRequestValue]?,
keys.compilerArgs: await self.buildSettings(for: snapshot.uri, fallbackAfterTimeout: true)?.compilerArgs
as [SKDRequestValue]?,
])
let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text)

View File

@@ -29,7 +29,9 @@ import SwiftSyntax
extension SwiftLanguageService {
/// Requests the semantic highlighting tokens for the given snapshot from sourcekitd.
private func semanticHighlightingTokens(for snapshot: DocumentSnapshot) async throws -> SyntaxHighlightingTokens? {
guard let buildSettings = await self.buildSettings(for: snapshot.uri), !buildSettings.isFallback else {
guard let buildSettings = await self.buildSettings(for: snapshot.uri, fallbackAfterTimeout: false),
!buildSettings.isFallback
else {
return nil
}

View File

@@ -259,7 +259,7 @@ package actor SwiftLanguageService: LanguageService, Sendable {
}
}
func buildSettings(for document: DocumentURI) async -> SwiftCompileCommand? {
func buildSettings(for document: DocumentURI, fallbackAfterTimeout: Bool) async -> SwiftCompileCommand? {
let primaryDocument = document.primaryFile ?? document
guard let sourceKitLSPServer else {
@@ -271,7 +271,8 @@ package actor SwiftLanguageService: LanguageService, Sendable {
}
if let settings = await workspace.buildSystemManager.buildSettingsInferredFromMainFile(
for: primaryDocument,
language: .swift
language: .swift,
fallbackAfterTimeout: fallbackAfterTimeout
) {
return SwiftCompileCommand(settings)
} else {
@@ -428,7 +429,7 @@ extension SwiftLanguageService {
try await self.sendSourcekitdRequest(closeReq, fileContents: nil)
}
let buildSettings = await buildSettings(for: snapshot.uri)
let buildSettings = await buildSettings(for: snapshot.uri, fallbackAfterTimeout: true)
let openReq = openDocumentSourcekitdRequest(
snapshot: snapshot,
compileCommand: buildSettings
@@ -450,7 +451,7 @@ extension SwiftLanguageService {
guard (try? documentManager.openDocuments.contains(uri)) ?? false else {
return
}
let newBuildSettings = await self.buildSettings(for: uri)
let newBuildSettings = await self.buildSettings(for: uri, fallbackAfterTimeout: false)
if newBuildSettings != buildSettingsForOpenFiles[uri] {
// Close and re-open the document internally to inform sourcekitd to update the compile command. At the moment
// there's no better way to do this.
@@ -516,7 +517,7 @@ extension SwiftLanguageService {
cancelInFlightPublishDiagnosticsTask(for: notification.textDocument.uri)
await diagnosticReportManager.removeItemsFromCache(with: notification.textDocument.uri)
let buildSettings = await self.buildSettings(for: snapshot.uri)
let buildSettings = await self.buildSettings(for: snapshot.uri, fallbackAfterTimeout: true)
buildSettingsForOpenFiles[snapshot.uri] = buildSettings
let req = openDocumentSourcekitdRequest(snapshot: snapshot, compileCommand: buildSettings)
@@ -578,7 +579,7 @@ extension SwiftLanguageService {
}
do {
let snapshot = try await self.latestSnapshot(for: document)
let buildSettings = await self.buildSettings(for: document)
let buildSettings = await self.buildSettings(for: document, fallbackAfterTimeout: false)
let diagnosticReport = try await self.diagnosticReportManager.diagnosticReport(
for: snapshot,
buildSettings: buildSettings
@@ -688,7 +689,8 @@ extension SwiftLanguageService {
package func hover(_ req: HoverRequest) async throws -> HoverResponse? {
let uri = req.textDocument.uri
let position = req.position
let cursorInfoResults = try await cursorInfo(uri, position..<position).cursorInfo
let cursorInfoResults = try await cursorInfo(uri, position..<position, fallbackSettingsAfterTimeout: false)
.cursorInfo
let symbolDocumentations = cursorInfoResults.compactMap { (cursorInfo) -> String? in
if let documentation = cursorInfo.documentation {
@@ -891,6 +893,7 @@ extension SwiftLanguageService {
let cursorInfoResponse = try await cursorInfo(
params.textDocument.uri,
params.range,
fallbackSettingsAfterTimeout: true,
additionalParameters: additionalCursorInfoParameters
)
@@ -917,7 +920,7 @@ extension SwiftLanguageService {
func retrieveQuickFixCodeActions(_ params: CodeActionRequest) async throws -> [CodeAction] {
let snapshot = try await self.latestSnapshot(for: params.textDocument.uri)
let buildSettings = await self.buildSettings(for: params.textDocument.uri)
let buildSettings = await self.buildSettings(for: params.textDocument.uri, fallbackAfterTimeout: true)
let diagnosticReport = try await self.diagnosticReportManager.diagnosticReport(
for: snapshot,
buildSettings: buildSettings
@@ -1010,7 +1013,8 @@ extension SwiftLanguageService {
req.textDocument.uri.primaryFile ?? req.textDocument.uri
)
let snapshot = try await self.latestSnapshot(for: req.textDocument.uri)
let buildSettings = await self.buildSettings(for: req.textDocument.uri)
let buildSettings = await self.buildSettings(for: req.textDocument.uri, fallbackAfterTimeout: false)
try Task.checkCancellation()
let diagnosticReport = try await self.diagnosticReportManager.diagnosticReport(
for: snapshot,
buildSettings: buildSettings

View File

@@ -21,6 +21,7 @@ extension SwiftLanguageService {
let uri = req.textDocument.uri
let snapshot = try documentManager.latestSnapshot(uri)
let position = await self.adjustPositionToStartOfIdentifier(req.position, in: snapshot)
return try await cursorInfo(uri, position..<position).cursorInfo.map { $0.symbolInfo }
return try await cursorInfo(uri, position..<position, fallbackSettingsAfterTimeout: false)
.cursorInfo.map { $0.symbolInfo }
}
}

View File

@@ -89,7 +89,8 @@ extension SwiftLanguageService {
keys.request: requests.collectVariableType,
keys.sourceFile: snapshot.uri.sourcekitdSourceFile,
keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath,
keys.compilerArgs: await self.buildSettings(for: uri)?.compilerArgs as [SKDRequestValue]?,
keys.compilerArgs: await self.buildSettings(for: uri, fallbackAfterTimeout: false)?.compilerArgs
as [SKDRequestValue]?,
])
if let range = range {

View File

@@ -169,6 +169,8 @@ extension Collection where Element: Sendable {
package struct TimeoutError: Error, CustomStringConvertible {
package var description: String { "Timed out" }
package init() {}
}
/// Executes `body`. If it doesn't finish after `duration`, throws a `TimeoutError`.
@@ -219,3 +221,47 @@ package func withTimeout<T: Sendable>(
}
}
}
/// Executes `body`. If it doesn't finish after `duration`, return `nil` and continue running body. When `body` returns
/// a value after the timeout, `resultReceivedAfterTimeout` is called.
///
/// - Important: `body` will not be cancelled when the timeout is received. Use the other overload of `withTimeout` if
/// `body` should be cancelled after `timeout`.
package func withTimeout<T: Sendable>(
_ timeout: Duration,
body: @escaping @Sendable () async throws -> T?,
resultReceivedAfterTimeout: @escaping @Sendable () async -> Void
) async throws -> T? {
let didHitTimeout = AtomicBool(initialValue: false)
let stream = AsyncThrowingStream<T?, Error> { continuation in
Task {
try await Task.sleep(for: timeout)
didHitTimeout.value = true
continuation.yield(nil)
}
Task {
do {
let result = try await body()
if didHitTimeout.value {
await resultReceivedAfterTimeout()
}
continuation.yield(result)
} catch {
continuation.yield(with: .failure(error))
}
}
}
for try await value in stream {
return value
}
// The only reason for the loop above to terminate is if the Task got cancelled or if the continuation finishes
// (which it never does).
if Task.isCancelled {
throw CancellationError()
} else {
preconditionFailure("Continuation never finishes")
}
}

View File

@@ -118,7 +118,11 @@ final class BuildSystemManagerTests: XCTestCase {
// Wait for the new build settings to settle before registering for change notifications
await bsm.waitForUpToDateBuildGraph()
await bsm.registerForChangeNotifications(for: a, language: .swift)
assertEqual(await bsm.buildSettingsInferredFromMainFile(for: a, language: .swift)?.compilerArguments, ["x"])
assertEqual(
await bsm.buildSettingsInferredFromMainFile(for: a, language: .swift, fallbackAfterTimeout: false)?
.compilerArguments,
["x"]
)
let changed = expectation(description: "changed settings")
await del.setExpected([
@@ -166,7 +170,10 @@ final class BuildSystemManagerTests: XCTestCase {
let del = await BSMDelegate(bsm)
let fallbackSettings = fallbackBuildSettings(for: a, language: .swift, options: .init())
await bsm.registerForChangeNotifications(for: a, language: .swift)
assertEqual(await bsm.buildSettingsInferredFromMainFile(for: a, language: .swift), fallbackSettings)
assertEqual(
await bsm.buildSettingsInferredFromMainFile(for: a, language: .swift, fallbackAfterTimeout: false),
fallbackSettings
)
let changed = expectation(description: "changed settings")
await del.setExpected([(a, .swift, FileBuildSettings(compilerArguments: ["non-fallback", "args"]), changed)])
@@ -212,7 +219,10 @@ final class BuildSystemManagerTests: XCTestCase {
// Wait for the new build settings to settle before registering for change notifications
await bsm.waitForUpToDateBuildGraph()
await bsm.registerForChangeNotifications(for: h, language: .c)
assertEqual(await bsm.buildSettingsInferredFromMainFile(for: h, language: .c)?.compilerArguments, ["C++ 1"])
assertEqual(
await bsm.buildSettingsInferredFromMainFile(for: h, language: .c, fallbackAfterTimeout: false)?.compilerArguments,
["C++ 1"]
)
await mainFiles.updateMainFiles(for: h, to: [cpp2])
@@ -281,8 +291,14 @@ final class BuildSystemManagerTests: XCTestCase {
let expectedArgsH1 = FileBuildSettings(compilerArguments: ["-xc++", cppArg, h1.pseudoPath])
let expectedArgsH2 = FileBuildSettings(compilerArguments: ["-xc++", cppArg, h2.pseudoPath])
assertEqual(await bsm.buildSettingsInferredFromMainFile(for: h1, language: .c), expectedArgsH1)
assertEqual(await bsm.buildSettingsInferredFromMainFile(for: h2, language: .c), expectedArgsH2)
assertEqual(
await bsm.buildSettingsInferredFromMainFile(for: h1, language: .c, fallbackAfterTimeout: false),
expectedArgsH1
)
assertEqual(
await bsm.buildSettingsInferredFromMainFile(for: h2, language: .c, fallbackAfterTimeout: false),
expectedArgsH2
)
let newCppArg = "New C++ Main File"
let changed1 = expectation(description: "initial settings h1 via cpp")
@@ -362,7 +378,11 @@ private actor BSMDelegate: BuildSystemManagerDelegate {
self.expected.remove(at: expectedIndex)
XCTAssertEqual(uri, expected.uri, file: expected.file, line: expected.line)
let settings = await bsm.buildSettingsInferredFromMainFile(for: uri, language: expected.language)
let settings = await bsm.buildSettingsInferredFromMainFile(
for: uri,
language: expected.language,
fallbackAfterTimeout: false
)
XCTAssertEqual(settings, expected.settings, file: expected.file, line: expected.line)
expected.expectation.fulfill()
}

View File

@@ -10,10 +10,12 @@
//
//===----------------------------------------------------------------------===//
import BuildServerProtocol
@_spi(Testing) import BuildSystemIntegration
import LanguageServerProtocol
import SKOptions
import SKTestSupport
import SourceKitLSP
import TSCBasic
import XCTest
@@ -160,4 +162,42 @@ final class FallbackBuildSystemTests: XCTestCase {
XCTAssertNil(fallbackBuildSettings(for: source, language: Language(rawValue: "unknown"), options: .init()))
}
func testFallbackBuildSettingsWhileBuildSystemIsComputingBuildSettings() async throws {
let fallbackResultsReceived = WrappedSemaphore(name: "Fallback results received")
let project = try await SwiftPMTestProject(
files: [
"Test.swift": """
let x: 1⃣String2⃣ = 1
"""
],
testHooks: TestHooks(
buildSystemTestHooks: BuildSystemTestHooks(
handleRequest: { request in
if request is TextDocumentSourceKitOptionsRequest {
fallbackResultsReceived.waitOrXCTFail()
}
}
)
)
)
let (uri, positions) = try project.openDocument("Test.swift")
let documentHighlight = try await project.testClient.send(
DocumentHighlightRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1"])
)
XCTAssertEqual(documentHighlight, [DocumentHighlight(range: positions["1"]..<positions["2"], kind: .read)])
fallbackResultsReceived.signal()
try await repeatUntilExpectedResult {
let diagsAfterBuildSettings = try await project.testClient.send(
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
)
return diagsAfterBuildSettings.fullReport?.items.map(\.message) == [
"Cannot convert value of type 'Int' to specified type 'String'"
]
}
}
}

View File

@@ -121,7 +121,11 @@ final class SwiftPMBuildSystemTests: XCTestCase {
assertNotNil(await buildSystemManager.initializationData?.indexDatabasePath)
assertNotNil(await buildSystemManager.initializationData?.indexStorePath)
let arguments = try await unwrap(
buildSystemManager.buildSettingsInferredFromMainFile(for: aswift.asURI, language: .swift)
buildSystemManager.buildSettingsInferredFromMainFile(
for: aswift.asURI,
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
assertArgumentsContain("-module-name", "lib", arguments: arguments)
@@ -191,7 +195,8 @@ final class SwiftPMBuildSystemTests: XCTestCase {
let arguments = try await unwrap(
buildSystemManager.buildSettingsInferredFromMainFile(
for: DocumentURI(urlWithPlusEscaped),
language: .swift
language: .swift,
fallbackAfterTimeout: false
)
)
.compilerArguments
@@ -246,7 +251,11 @@ final class SwiftPMBuildSystemTests: XCTestCase {
let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift")
let arguments = try await unwrap(
buildSystemManager.buildSettingsInferredFromMainFile(for: aswift.asURI, language: .swift)
buildSystemManager.buildSettingsInferredFromMainFile(
for: aswift.asURI,
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
assertArgumentsContain("-typecheck", arguments: arguments)
@@ -322,7 +331,11 @@ final class SwiftPMBuildSystemTests: XCTestCase {
let source = try resolveSymlinks(packageRoot.appending(component: "Package.swift"))
let arguments = try await unwrap(
buildSystemManager.buildSettingsInferredFromMainFile(for: source.asURI, language: .swift)
buildSystemManager.buildSettingsInferredFromMainFile(
for: source.asURI,
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
assertArgumentsContain("-swift-version", "4.2", arguments: arguments)
@@ -360,12 +373,20 @@ final class SwiftPMBuildSystemTests: XCTestCase {
let bswift = packageRoot.appending(components: "Sources", "lib", "b.swift")
let argumentsA = try await unwrap(
buildSystemManager.buildSettingsInferredFromMainFile(for: aswift.asURI, language: .swift)
buildSystemManager.buildSettingsInferredFromMainFile(
for: aswift.asURI,
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
assertArgumentsContain(aswift.pathString, arguments: argumentsA)
assertArgumentsContain(bswift.pathString, arguments: argumentsA)
let argumentsB = try await unwrap(
buildSystemManager.buildSettingsInferredFromMainFile(for: aswift.asURI, language: .swift)
buildSystemManager.buildSettingsInferredFromMainFile(
for: aswift.asURI,
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
assertArgumentsContain(aswift.pathString, arguments: argumentsB)
assertArgumentsContain(bswift.pathString, arguments: argumentsB)
@@ -408,7 +429,11 @@ final class SwiftPMBuildSystemTests: XCTestCase {
let aswift = packageRoot.appending(components: "Sources", "libA", "a.swift")
let bswift = packageRoot.appending(components: "Sources", "libB", "b.swift")
let arguments = try await unwrap(
buildSystemManager.buildSettingsInferredFromMainFile(for: aswift.asURI, language: .swift)
buildSystemManager.buildSettingsInferredFromMainFile(
for: aswift.asURI,
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
assertArgumentsContain(aswift.pathString, arguments: arguments)
assertArgumentsDoNotContain(bswift.pathString, arguments: arguments)
@@ -421,7 +446,11 @@ final class SwiftPMBuildSystemTests: XCTestCase {
)
let argumentsB = try await unwrap(
buildSystemManager.buildSettingsInferredFromMainFile(for: bswift.asURI, language: .swift)
buildSystemManager.buildSettingsInferredFromMainFile(
for: bswift.asURI,
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
assertArgumentsContain(bswift.pathString, arguments: argumentsB)
assertArgumentsDoNotContain(aswift.pathString, arguments: argumentsB)
@@ -462,15 +491,26 @@ final class SwiftPMBuildSystemTests: XCTestCase {
let aswift = packageRoot.appending(components: "Sources", "libA", "a.swift")
let bswift = packageRoot.appending(components: "Sources", "libB", "b.swift")
assertNotNil(await buildSystemManager.buildSettingsInferredFromMainFile(for: aswift.asURI, language: .swift))
assertNotNil(
await buildSystemManager.buildSettingsInferredFromMainFile(
for: aswift.asURI,
language: .swift,
fallbackAfterTimeout: false
)
)
assertEqual(
await buildSystemManager.buildSettingsInferredFromMainFile(for: bswift.asURI, language: .swift)?.isFallback,
await buildSystemManager.buildSettingsInferredFromMainFile(
for: bswift.asURI,
language: .swift,
fallbackAfterTimeout: false
)?.isFallback,
true
)
assertEqual(
await buildSystemManager.buildSettingsInferredFromMainFile(
for: DocumentURI(URL(string: "https://www.apple.com")!),
language: .swift
language: .swift,
fallbackAfterTimeout: false
)?.isFallback,
true
)
@@ -515,7 +555,11 @@ final class SwiftPMBuildSystemTests: XCTestCase {
for file in [acxx, header] {
let args = try await unwrap(
buildSystemManager.buildSettingsInferredFromMainFile(for: file.asURI, language: .cpp)
buildSystemManager.buildSettingsInferredFromMainFile(
for: file.asURI,
language: .cpp,
fallbackAfterTimeout: false
)
).compilerArguments
assertArgumentsContain("-std=c++14", arguments: args)
@@ -588,7 +632,11 @@ final class SwiftPMBuildSystemTests: XCTestCase {
let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift")
let arguments = try await unwrap(
buildSystemManager.buildSettingsInferredFromMainFile(for: aswift.asURI, language: .swift)
buildSystemManager.buildSettingsInferredFromMainFile(
for: aswift.asURI,
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
assertArgumentsContain("-target", arguments: arguments) // Only one!
@@ -642,10 +690,18 @@ final class SwiftPMBuildSystemTests: XCTestCase {
let manifest = packageRoot.appending(components: "Package.swift")
let argumentsFromSymlink = try await unwrap(
buildSystemManager.buildSettingsInferredFromMainFile(for: aswiftSymlink.asURI, language: .swift)
buildSystemManager.buildSettingsInferredFromMainFile(
for: aswiftSymlink.asURI,
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
let argumentsFromReal = try await unwrap(
buildSystemManager.buildSettingsInferredFromMainFile(for: aswiftReal.asURI, language: .swift)
buildSystemManager.buildSettingsInferredFromMainFile(
for: aswiftReal.asURI,
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
// The arguments retrieved from the symlink and the real document should be the same, except that both should
@@ -663,7 +719,11 @@ final class SwiftPMBuildSystemTests: XCTestCase {
assertArgumentsDoNotContain(aswiftSymlink.pathString, arguments: argumentsFromReal)
let argsManifest = try await unwrap(
buildSystemManager.buildSettingsInferredFromMainFile(for: manifest.asURI, language: .swift)
buildSystemManager.buildSettingsInferredFromMainFile(
for: manifest.asURI,
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
XCTAssertNotNil(argsManifest)
@@ -717,7 +777,8 @@ final class SwiftPMBuildSystemTests: XCTestCase {
let args = try unwrap(
await buildSystemManager.buildSettingsInferredFromMainFile(
for: symlinkRoot.appending(components: file).asURI,
language: .cpp
language: .cpp,
fallbackAfterTimeout: false
)?
.compilerArguments
)
@@ -756,7 +817,11 @@ final class SwiftPMBuildSystemTests: XCTestCase {
let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift")
let arguments = try await unwrap(
buildSystemManager.buildSettingsInferredFromMainFile(for: aswift.asURI, language: .swift)
buildSystemManager.buildSettingsInferredFromMainFile(
for: aswift.asURI,
language: .swift,
fallbackAfterTimeout: false
)
)
.compilerArguments
assertArgumentsContain(aswift.pathString, arguments: arguments)
@@ -830,7 +895,11 @@ final class SwiftPMBuildSystemTests: XCTestCase {
assertNotNil(await buildSystemManager.initializationData?.indexStorePath)
let arguments = try await unwrap(
buildSystemManager.buildSettingsInferredFromMainFile(for: aswift.asURI, language: .swift)
buildSystemManager.buildSettingsInferredFromMainFile(
for: aswift.asURI,
language: .swift,
fallbackAfterTimeout: false
)
).compilerArguments
// Plugins get compiled with the same compiler arguments as the package manifest