From 69ab356b363db4cfc89578c36c31a86c18cdf837 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Thu, 27 Mar 2025 08:35:40 -0700 Subject: [PATCH 01/13] =?UTF-8?q?Log=20contextual=20requests=20that=20affe?= =?UTF-8?q?ct=20sourcekitd=E2=80=99s=20global=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This way we can log them when a sourcekitd request crashes and we can thus replay these contextual requests when diagnosing the crash. --- Package.swift | 1 + Sources/SKTestSupport/SkipUnless.swift | 4 +- Sources/SKTestSupport/SourceKitD+send.swift | 6 +- Sources/SourceKitD/SourceKitD.swift | 67 +++++++++++++++++-- Sources/SourceKitLSP/Rename.swift | 9 +-- .../Swift/CodeCompletionSession.swift | 27 +++++--- Sources/SourceKitLSP/Swift/CursorInfo.swift | 3 +- .../Swift/DiagnosticReportManager.swift | 3 +- .../Swift/GeneratedInterfaceManager.swift | 22 +++--- .../SourceKitLSP/Swift/MacroExpansion.swift | 6 +- .../Swift/RefactoringResponse.swift | 3 +- .../Swift/RelatedIdentifiers.swift | 3 +- .../SourceKitLSP/Swift/SemanticTokens.swift | 3 +- .../Swift/SwiftLanguageService.swift | 32 ++++----- .../SourceKitLSP/Swift/VariableTypeInfo.swift | 3 +- ...thSnapshotFromDiskOpenedInSourcekitd.swift | 10 +-- Tests/SourceKitDTests/SourceKitDTests.swift | 8 +-- .../SwiftSourceKitPluginTests.swift | 50 +++++--------- 18 files changed, 146 insertions(+), 114 deletions(-) diff --git a/Package.swift b/Package.swift index e0c2e0f3f..de47a9018 100644 --- a/Package.swift +++ b/Package.swift @@ -429,6 +429,7 @@ var targets: [Target] = [ dependencies: [ "BuildSystemIntegration", "CSKTestSupport", + "Csourcekitd", "InProcessClient", "LanguageServerProtocol", "LanguageServerProtocolExtensions", diff --git a/Sources/SKTestSupport/SkipUnless.swift b/Sources/SKTestSupport/SkipUnless.swift index 1c8830e23..ede127af3 100644 --- a/Sources/SKTestSupport/SkipUnless.swift +++ b/Sources/SKTestSupport/SkipUnless.swift @@ -259,11 +259,11 @@ package actor SkipUnless { ) do { let response = try await sourcekitd.send( + \.codeCompleteSetPopularAPI, sourcekitd.dictionary([ - sourcekitd.keys.request: sourcekitd.requests.codeCompleteSetPopularAPI, sourcekitd.keys.codeCompleteOptions: [ sourcekitd.keys.useNewAPI: 1 - ], + ] ]), timeout: defaultTimeoutDuration ) diff --git a/Sources/SKTestSupport/SourceKitD+send.swift b/Sources/SKTestSupport/SourceKitD+send.swift index c27287ab9..7879b36bb 100644 --- a/Sources/SKTestSupport/SourceKitD+send.swift +++ b/Sources/SKTestSupport/SourceKitD+send.swift @@ -10,19 +10,23 @@ // //===----------------------------------------------------------------------===// +package import Csourcekitd package import SourceKitD extension SourceKitD { /// Convenience overload of the `send` function for testing that doesn't restart sourcekitd if it does not respond /// and doesn't pass any file contents. package func send( + _ requestUid: KeyPath, _ request: SKDRequestDictionary, - timeout: Duration + timeout: Duration = defaultTimeoutDuration ) async throws -> SKDResponseDictionary { return try await self.send( + requestUid, request, timeout: timeout, restartTimeout: .seconds(60 * 60 * 24), + documentUrl: nil, fileContents: nil ) } diff --git a/Sources/SourceKitD/SourceKitD.swift b/Sources/SourceKitD/SourceKitD.swift index 48fd94cab..a296946e9 100644 --- a/Sources/SourceKitD/SourceKitD.swift +++ b/Sources/SourceKitD/SourceKitD.swift @@ -315,6 +315,43 @@ package actor SourceKitD { } } + private struct ContextualRequest { + enum Kind { + case editorOpen + case codeCompleteOpen + } + let kind: Kind + let request: SKDRequestDictionary + } + + private var contextualRequests: [URL: [ContextualRequest]] = [:] + + private func recordContextualRequest( + requestUid: sourcekitd_api_uid_t, + request: SKDRequestDictionary, + documentUrl: URL? + ) { + guard let documentUrl else { + return + } + switch requestUid { + case requests.editorOpen: + contextualRequests[documentUrl] = [ContextualRequest(kind: .editorOpen, request: request)] + case requests.editorClose: + contextualRequests[documentUrl] = nil + case requests.codeCompleteOpen: + contextualRequests[documentUrl, default: []].removeAll(where: { $0.kind == .codeCompleteOpen }) + contextualRequests[documentUrl, default: []].append(ContextualRequest(kind: .codeCompleteOpen, request: request)) + case requests.codeCompleteClose: + contextualRequests[documentUrl, default: []].removeAll(where: { $0.kind == .codeCompleteOpen }) + if contextualRequests[documentUrl]?.isEmpty ?? false { + contextualRequests[documentUrl] = nil + } + default: + break + } + } + /// - Parameters: /// - request: The request to send to sourcekitd. /// - timeout: The maximum duration how long to wait for a response. If no response is returned within this time, @@ -322,11 +359,16 @@ package actor SourceKitD { /// - fileContents: The contents of the file that the request operates on. If sourcekitd crashes, the file contents /// will be logged. package func send( + _ requestUid: KeyPath, _ request: SKDRequestDictionary, timeout: Duration, restartTimeout: Duration, + documentUrl: URL?, fileContents: String? ) async throws -> SKDResponseDictionary { + request.set(keys.request, to: requests[keyPath: requestUid]) + recordContextualRequest(requestUid: requests[keyPath: requestUid], request: request, documentUrl: documentUrl) + let sourcekitdResponse = try await withTimeout(timeout) { let restartTimeoutHandle = TimeoutHandle() do { @@ -387,13 +429,24 @@ package actor SourceKitD { guard let dict = sourcekitdResponse.value else { if sourcekitdResponse.error == .connectionInterrupted { - let log = """ + var log = """ Request: \(request.description) File contents: \(fileContents ?? "") """ + + if let documentUrl { + let contextualRequests = (contextualRequests[documentUrl] ?? []).filter { $0.request !== request } + for (index, contextualRequest) in contextualRequests.enumerated() { + log += """ + + Contextual request \(index + 1) / \(contextualRequests.count): + \(contextualRequest.request.description) + """ + } + } let chunks = splitLongMultilineMessage(message: log) for (index, chunk) in chunks.enumerated() { logger.fault( @@ -414,10 +467,14 @@ package actor SourceKitD { } package func crash() async { - let req = dictionary([ - keys.request: requests.crashWithExit - ]) - _ = try? await send(req, timeout: .seconds(60), restartTimeout: .seconds(24 * 60 * 60), fileContents: nil) + _ = try? await send( + \.crashWithExit, + dictionary([:]), + timeout: .seconds(60), + restartTimeout: .seconds(24 * 60 * 60), + documentUrl: nil, + fileContents: nil + ) } } diff --git a/Sources/SourceKitLSP/Rename.swift b/Sources/SourceKitLSP/Rename.swift index b08fe91b6..f566b277a 100644 --- a/Sources/SourceKitLSP/Rename.swift +++ b/Sources/SourceKitLSP/Rename.swift @@ -353,7 +353,6 @@ extension SwiftLanguageService { } let req = sourcekitd.dictionary([ - keys.request: sourcekitd.requests.nameTranslation, keys.sourceFile: snapshot.uri.pseudoPath, keys.compilerArgs: await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: false)?.compilerArgs as [SKDRequestValue]?, @@ -363,7 +362,7 @@ extension SwiftLanguageService { keys.argNames: sourcekitd.array(name.parameters.map { $0.stringOrWildcard }), ]) - let response = try await sendSourcekitdRequest(req, fileContents: snapshot.text) + let response = try await send(sourcekitdRequest: \.nameTranslation, req, snapshot: snapshot) guard let isZeroArgSelector: Int = response[keys.isZeroArgSelector], let selectorPieces: SKDResponseArray = response[keys.selectorPieces] @@ -405,7 +404,6 @@ extension SwiftLanguageService { name: String ) async throws -> String { let req = sourcekitd.dictionary([ - keys.request: sourcekitd.requests.nameTranslation, keys.sourceFile: snapshot.uri.pseudoPath, keys.compilerArgs: await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: false)?.compilerArgs as [SKDRequestValue]?, @@ -421,7 +419,7 @@ extension SwiftLanguageService { req.set(keys.baseName, to: name) } - let response = try await sendSourcekitdRequest(req, fileContents: snapshot.text) + let response = try await send(sourcekitdRequest: \.nameTranslation, req, snapshot: snapshot) guard let baseName: String = response[keys.baseName] else { throw NameTranslationError.malformedClangToSwiftTranslateNameResponse(response) @@ -886,7 +884,6 @@ extension SwiftLanguageService { ) let skreq = sourcekitd.dictionary([ - keys.request: requests.findRenameRanges, keys.sourceFile: snapshot.uri.pseudoPath, // find-syntactic-rename-ranges is a syntactic sourcekitd request that doesn't use the in-memory file snapshot. // We need to send the source text again. @@ -894,7 +891,7 @@ extension SwiftLanguageService { keys.renameLocations: locations, ]) - let syntacticRenameRangesResponse = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text) + let syntacticRenameRangesResponse = try await send(sourcekitdRequest: \.findRenameRanges, skreq, snapshot: snapshot) guard let categorizedRanges: SKDResponseArray = syntacticRenameRangesResponse[keys.categorizedRanges] else { throw ResponseError.internalError("sourcekitd did not return categorized ranges") } diff --git a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift index 299785445..1d77fc41d 100644 --- a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift +++ b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift @@ -264,7 +264,6 @@ class CodeCompletionSession { self.clientSupportsDocumentationResolve = clientCapabilities.textDocument?.completion?.completionItem?.resolveSupport?.properties.contains("documentation") ?? false - } private func open( @@ -279,7 +278,6 @@ class CodeCompletionSession { let sourcekitdPosition = snapshot.sourcekitdPosition(of: self.position) let req = sourcekitd.dictionary([ - keys.request: sourcekitd.requests.codeCompleteOpen, keys.line: sourcekitdPosition.line, keys.column: sourcekitdPosition.utf8Column, keys.name: uri.pseudoPath, @@ -288,7 +286,7 @@ class CodeCompletionSession { keys.codeCompleteOptions: optionsDictionary(filterText: filterText), ]) - let dict = try await sendSourceKitdRequest(req, snapshot: snapshot) + let dict = try await send(sourceKitDRequest: \.codeCompleteOpen, req, snapshot: snapshot) self.state = .open guard let completions: SKDResponseArray = dict[keys.results] else { @@ -314,7 +312,6 @@ class CodeCompletionSession { logger.info("Updating code completion session: \(self.description) filter=\(filterText)") let sourcekitdPosition = snapshot.sourcekitdPosition(of: self.position) let req = sourcekitd.dictionary([ - keys.request: sourcekitd.requests.codeCompleteUpdate, keys.line: sourcekitdPosition.line, keys.column: sourcekitdPosition.utf8Column, keys.name: uri.pseudoPath, @@ -322,7 +319,7 @@ class CodeCompletionSession { keys.codeCompleteOptions: optionsDictionary(filterText: filterText), ]) - let dict = try await sendSourceKitdRequest(req, snapshot: snapshot) + let dict = try await send(sourceKitDRequest: \.codeCompleteUpdate, req, snapshot: snapshot) guard let completions: SKDResponseArray = dict[keys.results] else { return CompletionList(isIncomplete: false, items: []) } @@ -363,7 +360,6 @@ class CodeCompletionSession { case .open: let sourcekitdPosition = snapshot.sourcekitdPosition(of: self.position) let req = sourcekitd.dictionary([ - keys.request: sourcekitd.requests.codeCompleteClose, keys.line: sourcekitdPosition.line, keys.column: sourcekitdPosition.utf8Column, keys.sourceFile: snapshot.uri.pseudoPath, @@ -371,21 +367,24 @@ class CodeCompletionSession { keys.codeCompleteOptions: [keys.useNewAPI: 1], ]) logger.info("Closing code completion session: \(self.description)") - _ = try? await sendSourceKitdRequest(req, snapshot: nil) + _ = try? await send(sourceKitDRequest: \.codeCompleteClose, req, snapshot: nil) self.state = .closed } } // MARK: - Helpers - private func sendSourceKitdRequest( + private func send( + sourceKitDRequest requestUid: KeyPath & Sendable, _ request: SKDRequestDictionary, snapshot: DocumentSnapshot? ) async throws -> SKDResponseDictionary { try await sourcekitd.send( + requestUid, request, timeout: options.sourcekitdRequestTimeoutOrDefault, restartTimeout: options.semanticServiceRestartTimeoutOrDefault, + documentUrl: snapshot?.uri.arbitrarySchemeURL, fileContents: snapshot?.text ) } @@ -560,11 +559,17 @@ class CodeCompletionSession { var item = item if let itemId = CompletionItemData(fromLSPAny: item.data)?.itemId { let req = sourcekitd.dictionary([ - sourcekitd.keys.request: sourcekitd.requests.codeCompleteDocumentation, - sourcekitd.keys.identifier: itemId, + sourcekitd.keys.identifier: itemId ]) let documentationResponse = await orLog("Retrieving documentation for completion item") { - try await sourcekitd.send(req, timeout: timeout, restartTimeout: restartTimeout, fileContents: nil) + try await sourcekitd.send( + \.codeCompleteDocumentation, + req, + timeout: timeout, + restartTimeout: restartTimeout, + documentUrl: nil, + fileContents: nil + ) } if let docString: String = documentationResponse?[sourcekitd.keys.docBrief] { item.documentation = .markupContent(MarkupContent(kind: .markdown, value: docString)) diff --git a/Sources/SourceKitLSP/Swift/CursorInfo.swift b/Sources/SourceKitLSP/Swift/CursorInfo.swift index 67d954713..e82ccf295 100644 --- a/Sources/SourceKitLSP/Swift/CursorInfo.swift +++ b/Sources/SourceKitLSP/Swift/CursorInfo.swift @@ -158,7 +158,6 @@ extension SwiftLanguageService { let keys = self.keys let skreq = sourcekitd.dictionary([ - keys.request: requests.cursorInfo, keys.cancelOnSubsequentRequest: 0, keys.offset: offsetRange.lowerBound, keys.length: offsetRange.upperBound != offsetRange.lowerBound ? offsetRange.count : nil, @@ -170,7 +169,7 @@ extension SwiftLanguageService { appendAdditionalParameters?(skreq) - let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text) + let dict = try await send(sourcekitdRequest: \.cursorInfo, skreq, snapshot: snapshot) var cursorInfoResults: [CursorInfo] = [] if let cursorInfo = CursorInfo(dict, snapshot: snapshot, documentManager: documentManager, sourcekitd: sourcekitd) { diff --git a/Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift b/Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift index a8aa78159..8258010d0 100644 --- a/Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift +++ b/Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift @@ -107,7 +107,6 @@ actor DiagnosticReportManager { let keys = self.keys let skreq = sourcekitd.dictionary([ - keys.request: requests.diagnostics, keys.sourceFile: snapshot.uri.sourcekitdSourceFile, keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath, keys.compilerArgs: compilerArgs as [SKDRequestValue], @@ -116,9 +115,11 @@ actor DiagnosticReportManager { let dict: SKDResponseDictionary do { dict = try await self.sourcekitd.send( + \.diagnostics, skreq, timeout: options.sourcekitdRequestTimeoutOrDefault, restartTimeout: options.semanticServiceRestartTimeoutOrDefault, + documentUrl: snapshot.uri.arbitrarySchemeURL, fileContents: snapshot.text ) } catch SKDError.requestFailed(let sourcekitdError) { diff --git a/Sources/SourceKitLSP/Swift/GeneratedInterfaceManager.swift b/Sources/SourceKitLSP/Swift/GeneratedInterfaceManager.swift index 6cc7bd015..3164a758a 100644 --- a/Sources/SourceKitLSP/Swift/GeneratedInterfaceManager.swift +++ b/Sources/SourceKitLSP/Swift/GeneratedInterfaceManager.swift @@ -67,13 +67,13 @@ actor GeneratedInterfaceManager { let sourcekitd = swiftLanguageService.sourcekitd for documentToClose in documentsToClose { await orLog("Closing generated interface") { - _ = try await swiftLanguageService.sendSourcekitdRequest( + _ = try await swiftLanguageService.send( + sourcekitdRequest: \.editorClose, sourcekitd.dictionary([ - sourcekitd.keys.request: sourcekitd.requests.editorClose, sourcekitd.keys.name: documentToClose, sourcekitd.keys.cancelBuilds: 0, ]), - fileContents: nil + snapshot: nil ) } } @@ -114,7 +114,6 @@ actor GeneratedInterfaceManager { let keys = sourcekitd.keys let skreq = sourcekitd.dictionary([ - keys.request: sourcekitd.requests.editorOpenInterface, keys.moduleName: document.moduleName, keys.groupName: document.groupName, keys.name: document.sourcekitdDocumentName, @@ -123,7 +122,7 @@ actor GeneratedInterfaceManager { .compilerArgs as [SKDRequestValue]?, ]) - let dict = try await swiftLanguageService.sendSourcekitdRequest(skreq, fileContents: nil) + let dict = try await swiftLanguageService.send(sourcekitdRequest: \.editorOpenInterface, skreq, snapshot: nil) guard let contents: String = dict[keys.sourceText] else { throw ResponseError.unknown("sourcekitd response is missing sourceText") @@ -133,13 +132,13 @@ actor GeneratedInterfaceManager { // Another request raced us to create the generated interface. Discard what we computed here and return the cached // value. await orLog("Closing generated interface created during race") { - _ = try await swiftLanguageService.sendSourcekitdRequest( + _ = try await swiftLanguageService.send( + sourcekitdRequest: \.editorClose, sourcekitd.dictionary([ - keys.request: sourcekitd.requests.editorClose, keys.name: document.sourcekitdDocumentName, keys.cancelBuilds: 0, ]), - fileContents: nil + snapshot: nil ) } return cached @@ -191,12 +190,15 @@ actor GeneratedInterfaceManager { let sourcekitd = swiftLanguageService.sourcekitd let keys = sourcekitd.keys let skreq = sourcekitd.dictionary([ - keys.request: sourcekitd.requests.editorFindUSR, keys.sourceFile: document.sourcekitdDocumentName, keys.usr: usr, ]) - let dict = try await swiftLanguageService.sendSourcekitdRequest(skreq, fileContents: details.snapshot.text) + let dict = try await swiftLanguageService.send( + sourcekitdRequest: \.editorFindUSR, + skreq, + snapshot: details.snapshot + ) guard let offset: Int = dict[keys.offset] else { throw ResponseError.unknown("Missing key 'offset'") } diff --git a/Sources/SourceKitLSP/Swift/MacroExpansion.swift b/Sources/SourceKitLSP/Swift/MacroExpansion.swift index 8cd170fa5..03f505fbd 100644 --- a/Sources/SourceKitLSP/Swift/MacroExpansion.swift +++ b/Sources/SourceKitLSP/Swift/MacroExpansion.swift @@ -94,7 +94,6 @@ actor MacroExpansionManager { let length = snapshot.utf8OffsetRange(of: range).count let skreq = swiftLanguageService.sourcekitd.dictionary([ - keys.request: swiftLanguageService.requests.semanticRefactoring, // Preferred name for e.g. an extracted variable. // Empty string means sourcekitd chooses a name automatically. keys.name: "", @@ -108,10 +107,7 @@ actor MacroExpansionManager { keys.compilerArgs: buildSettings?.compilerArgs as [SKDRequestValue]?, ]) - let dict = try await swiftLanguageService.sendSourcekitdRequest( - skreq, - fileContents: snapshot.text - ) + let dict = try await swiftLanguageService.send(sourcekitdRequest: \.semanticRefactoring, skreq, snapshot: snapshot) guard let expansions = [RefactoringEdit](dict, snapshot, keys) else { throw SemanticRefactoringError.noEditsNeeded(snapshot.uri) } diff --git a/Sources/SourceKitLSP/Swift/RefactoringResponse.swift b/Sources/SourceKitLSP/Swift/RefactoringResponse.swift index 1b3f67b74..382595b76 100644 --- a/Sources/SourceKitLSP/Swift/RefactoringResponse.swift +++ b/Sources/SourceKitLSP/Swift/RefactoringResponse.swift @@ -117,7 +117,6 @@ extension SwiftLanguageService { let utf8Column = snapshot.lineTable.utf8ColumnAt(line: line, utf16Column: utf16Column) let skreq = sourcekitd.dictionary([ - keys.request: self.requests.semanticRefactoring, // Preferred name for e.g. an extracted variable. // Empty string means sourcekitd chooses a name automatically. keys.name: "", @@ -131,7 +130,7 @@ extension SwiftLanguageService { as [SKDRequestValue]?, ]) - let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text) + let dict = try await send(sourcekitdRequest: \.semanticRefactoring, skreq, snapshot: snapshot) guard let refactor = SemanticRefactoring(refactorCommand.title, dict, snapshot, self.keys) else { throw SemanticRefactoringError.noEditsNeeded(uri) } diff --git a/Sources/SourceKitLSP/Swift/RelatedIdentifiers.swift b/Sources/SourceKitLSP/Swift/RelatedIdentifiers.swift index 71662df52..229d28881 100644 --- a/Sources/SourceKitLSP/Swift/RelatedIdentifiers.swift +++ b/Sources/SourceKitLSP/Swift/RelatedIdentifiers.swift @@ -67,7 +67,6 @@ extension SwiftLanguageService { includeNonEditableBaseNames: Bool ) async throws -> RelatedIdentifiersResponse { let skreq = sourcekitd.dictionary([ - keys.request: requests.relatedIdents, keys.cancelOnSubsequentRequest: 0, keys.offset: snapshot.utf8Offset(of: position), keys.sourceFile: snapshot.uri.sourcekitdSourceFile, @@ -77,7 +76,7 @@ extension SwiftLanguageService { as [SKDRequestValue]?, ]) - let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text) + let dict = try await send(sourcekitdRequest: \.relatedIdents, skreq, snapshot: snapshot) guard let results: SKDResponseArray = dict[self.keys.results] else { throw ResponseError.internalError("sourcekitd response did not contain results") diff --git a/Sources/SourceKitLSP/Swift/SemanticTokens.swift b/Sources/SourceKitLSP/Swift/SemanticTokens.swift index 4679e5828..a23255aaa 100644 --- a/Sources/SourceKitLSP/Swift/SemanticTokens.swift +++ b/Sources/SourceKitLSP/Swift/SemanticTokens.swift @@ -27,13 +27,12 @@ extension SwiftLanguageService { } let skreq = sourcekitd.dictionary([ - keys.request: requests.semanticTokens, keys.sourceFile: snapshot.uri.sourcekitdSourceFile, keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath, keys.compilerArgs: compileCommand.compilerArgs as [SKDRequestValue], ]) - let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text) + let dict = try await send(sourcekitdRequest: \.semanticTokens, skreq, snapshot: snapshot) guard let skTokens: SKDResponseArray = dict[keys.semanticTokens] else { return nil diff --git a/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift b/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift index c4585e645..4e95267a1 100644 --- a/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift +++ b/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift @@ -304,15 +304,18 @@ package actor SwiftLanguageService: LanguageService, Sendable { } } - func sendSourcekitdRequest( + func send( + sourcekitdRequest requestUid: KeyPath & Sendable, _ request: SKDRequestDictionary, - fileContents: String? + snapshot: DocumentSnapshot? ) async throws -> SKDResponseDictionary { try await sourcekitd.send( + requestUid, request, timeout: options.sourcekitdRequestTimeoutOrDefault, restartTimeout: options.semanticServiceRestartTimeoutOrDefault, - fileContents: fileContents + documentUrl: snapshot?.uri.arbitrarySchemeURL, + fileContents: snapshot?.text ) } @@ -429,10 +432,7 @@ extension SwiftLanguageService { /// Tell sourcekitd to crash itself. For testing purposes only. package func crash() async { - let req = sourcekitd.dictionary([ - keys.request: sourcekitd.requests.crashWithExit - ]) - _ = try? await sendSourcekitdRequest(req, fileContents: nil) + _ = try? await send(sourcekitdRequest: \.crashWithExit, sourcekitd.dictionary([:]), snapshot: nil) } // MARK: - Build System Integration @@ -455,7 +455,7 @@ extension SwiftLanguageService { let closeReq = closeDocumentSourcekitdRequest(uri: snapshot.uri) _ = await orLog("Closing document to re-open it") { - try await self.sendSourcekitdRequest(closeReq, fileContents: nil) + try await self.send(sourcekitdRequest: \.editorClose, closeReq, snapshot: nil) } let buildSettings = await compileCommand(for: snapshot.uri, fallbackAfterTimeout: true) @@ -465,7 +465,7 @@ extension SwiftLanguageService { ) self.buildSettingsForOpenFiles[snapshot.uri] = buildSettings _ = await orLog("Re-opening document") { - try await self.sendSourcekitdRequest(openReq, fileContents: snapshot.text) + try await self.send(sourcekitdRequest: \.editorOpen, openReq, snapshot: snapshot) } if await capabilityRegistry.clientSupportsPullDiagnostics(for: .swift) { @@ -497,10 +497,7 @@ extension SwiftLanguageService { } await orLog("Sending dependencyUpdated request to sourcekitd") { - let req = sourcekitd.dictionary([ - keys.request: requests.dependencyUpdated - ]) - _ = try await self.sendSourcekitdRequest(req, fileContents: nil) + _ = try await self.send(sourcekitdRequest: \.dependencyUpdated, sourcekitd.dictionary([:]), snapshot: nil) } // Even after sending the `dependencyUpdated` request to sourcekitd, the code completion session has state from // before the AST update. Close it and open a new code completion session on the next completion request. @@ -519,7 +516,6 @@ extension SwiftLanguageService { compileCommand: SwiftCompileCommand? ) -> SKDRequestDictionary { return sourcekitd.dictionary([ - keys.request: self.requests.editorOpen, keys.name: snapshot.uri.pseudoPath, keys.sourceText: snapshot.text, keys.enableSyntaxMap: 0, @@ -532,7 +528,6 @@ extension SwiftLanguageService { func closeDocumentSourcekitdRequest(uri: DocumentURI) -> SKDRequestDictionary { return sourcekitd.dictionary([ - keys.request: requests.editorClose, keys.name: uri.pseudoPath, keys.cancelBuilds: 0, ]) @@ -555,7 +550,7 @@ extension SwiftLanguageService { let req = openDocumentSourcekitdRequest(snapshot: snapshot, compileCommand: buildSettings) await orLog("Opening sourcekitd document") { - _ = try await self.sendSourcekitdRequest(req, fileContents: snapshot.text) + _ = try await self.send(sourcekitdRequest: \.editorOpen, req, snapshot: snapshot) } await publishDiagnosticsIfNeeded(for: notification.textDocument.uri) } @@ -574,7 +569,7 @@ extension SwiftLanguageService { case nil: let req = closeDocumentSourcekitdRequest(uri: notification.textDocument.uri) await orLog("Closing sourcekitd document") { - _ = try await self.sendSourcekitdRequest(req, fileContents: nil) + _ = try await self.send(sourcekitdRequest: \.editorClose, req, snapshot: nil) } } } @@ -672,7 +667,6 @@ extension SwiftLanguageService { for edit in edits { let req = sourcekitd.dictionary([ - keys.request: self.requests.editorReplaceText, keys.name: notification.textDocument.uri.pseudoPath, keys.enableSyntaxMap: 0, keys.enableStructure: 0, @@ -683,7 +677,7 @@ extension SwiftLanguageService { keys.sourceText: edit.replacement, ]) do { - _ = try await self.sendSourcekitdRequest(req, fileContents: nil) + _ = try await self.send(sourcekitdRequest: \.editorReplaceText, req, snapshot: nil) } catch { logger.fault( """ diff --git a/Sources/SourceKitLSP/Swift/VariableTypeInfo.swift b/Sources/SourceKitLSP/Swift/VariableTypeInfo.swift index 7420b0f42..a13409c53 100644 --- a/Sources/SourceKitLSP/Swift/VariableTypeInfo.swift +++ b/Sources/SourceKitLSP/Swift/VariableTypeInfo.swift @@ -86,7 +86,6 @@ extension SwiftLanguageService { let snapshot = try await self.latestSnapshot(for: uri) let skreq = sourcekitd.dictionary([ - keys.request: requests.collectVariableType, keys.sourceFile: snapshot.uri.sourcekitdSourceFile, keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath, keys.compilerArgs: await self.compileCommand(for: uri, fallbackAfterTimeout: false)?.compilerArgs @@ -100,7 +99,7 @@ extension SwiftLanguageService { skreq.set(keys.length, to: end - start) } - let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text) + let dict = try await send(sourcekitdRequest: \.collectVariableType, skreq, snapshot: snapshot) guard let skVariableTypeInfos: SKDResponseArray = dict[keys.variableTypeList] else { return [] } diff --git a/Sources/SourceKitLSP/Swift/WithSnapshotFromDiskOpenedInSourcekitd.swift b/Sources/SourceKitLSP/Swift/WithSnapshotFromDiskOpenedInSourcekitd.swift index 6348b8015..27918784b 100644 --- a/Sources/SourceKitLSP/Swift/WithSnapshotFromDiskOpenedInSourcekitd.swift +++ b/Sources/SourceKitLSP/Swift/WithSnapshotFromDiskOpenedInSourcekitd.swift @@ -46,9 +46,10 @@ extension SwiftLanguageService { nil } - _ = try await sendSourcekitdRequest( + _ = try await send( + sourcekitdRequest: \.editorOpen, self.openDocumentSourcekitdRequest(snapshot: snapshot, compileCommand: patchedCompileCommand), - fileContents: snapshot.text + snapshot: snapshot ) let result: Swift.Result do { @@ -57,9 +58,10 @@ extension SwiftLanguageService { result = .failure(error) } await orLog("Close helper document '\(snapshot.uri)' for cursorInfoFromDisk") { - _ = try await sendSourcekitdRequest( + _ = try await send( + sourcekitdRequest: \.editorClose, self.closeDocumentSourcekitdRequest(uri: snapshot.uri), - fileContents: snapshot.text + snapshot: snapshot ) } return try result.get() diff --git a/Tests/SourceKitDTests/SourceKitDTests.swift b/Tests/SourceKitDTests/SourceKitDTests.swift index 8a9650cd2..137a50101 100644 --- a/Tests/SourceKitDTests/SourceKitDTests.swift +++ b/Tests/SourceKitDTests/SourceKitDTests.swift @@ -76,7 +76,6 @@ final class SourceKitDTests: XCTestCase { args.append(path) let req = sourcekitd.dictionary([ - keys.request: sourcekitd.requests.editorOpen, keys.name: path, keys.sourceText: """ func foo() {} @@ -84,15 +83,14 @@ final class SourceKitDTests: XCTestCase { keys.compilerArgs: args, ]) - _ = try await sourcekitd.send(req, timeout: defaultTimeoutDuration) + _ = try await sourcekitd.send(\.editorOpen, req, timeout: defaultTimeoutDuration) try await fulfillmentOfOrThrow(expectation1, expectation2) let close = sourcekitd.dictionary([ - keys.request: sourcekitd.requests.editorClose, - keys.name: path, + keys.name: path ]) - _ = try await sourcekitd.send(close, timeout: defaultTimeoutDuration) + _ = try await sourcekitd.send(\.editorClose, close, timeout: defaultTimeoutDuration) } } diff --git a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift index b99e69b36..59bd49884 100644 --- a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift +++ b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift @@ -1824,19 +1824,17 @@ fileprivate extension SourceKitD { compilerArguments += ["-sdk", defaultSDKPath] } let req = dictionary([ - keys.request: requests.editorOpen, keys.name: name, keys.sourceText: textWithoutMarkers, keys.syntacticOnly: 1, keys.compilerArgs: compilerArguments as [SKDRequestValue], ]) - _ = try await send(req, timeout: defaultTimeoutDuration) + _ = try await send(\.editorOpen, req) return DocumentPositions(markers: markers, textWithoutMarkers: textWithoutMarkers) } nonisolated func editDocument(_ name: String, fromOffset offset: Int, length: Int, newContents: String) async throws { let req = dictionary([ - keys.request: requests.editorReplaceText, keys.name: name, keys.offset: offset, keys.length: length, @@ -1844,20 +1842,19 @@ fileprivate extension SourceKitD { keys.syntacticOnly: 1, ]) - _ = try await send(req, timeout: defaultTimeoutDuration) + _ = try await send(\.editorReplaceText, req) } nonisolated func closeDocument(_ name: String) async throws { let req = dictionary([ - keys.request: requests.editorClose, - keys.name: name, + keys.name: name ]) - _ = try await send(req, timeout: defaultTimeoutDuration) + _ = try await send(\.editorClose, req) } nonisolated func completeImpl( - requestUID: sourcekitd_api_uid_t, + requestUID: KeyPath & Sendable, path: String, position: Position, filter: String, @@ -1879,7 +1876,6 @@ fileprivate extension SourceKitD { ]) let req = dictionary([ - keys.request: requestUID, keys.line: position.line + 1, // Technically sourcekitd needs a UTF-8 index but we can assume there are no Unicode characters in the tests keys.column: position.utf16index + 1, @@ -1888,7 +1884,7 @@ fileprivate extension SourceKitD { keys.compilerArgs: compilerArguments as [SKDRequestValue]?, ]) - let res = try await send(req, timeout: defaultTimeoutDuration) + let res = try await send(requestUID, req) return try CompletionResultSet(res) } @@ -1903,7 +1899,7 @@ fileprivate extension SourceKitD { compilerArguments: [String]? = nil ) async throws -> CompletionResultSet { return try await completeImpl( - requestUID: requests.codeCompleteOpen, + requestUID: \.codeCompleteOpen, path: path, position: position, filter: filter, @@ -1924,7 +1920,7 @@ fileprivate extension SourceKitD { maxResults: Int? = nil ) async throws -> CompletionResultSet { return try await completeImpl( - requestUID: requests.codeCompleteUpdate, + requestUID: \.codeCompleteUpdate, path: path, position: position, filter: filter, @@ -1938,7 +1934,6 @@ fileprivate extension SourceKitD { nonisolated func completeClose(path: String, position: Position) async throws { let req = dictionary([ - keys.request: requests.codeCompleteClose, keys.line: position.line + 1, // Technically sourcekitd needs a UTF-8 index but we can assume there are no Unicode characters in the tests keys.column: position.utf16index + 1, @@ -1946,45 +1941,32 @@ fileprivate extension SourceKitD { keys.codeCompleteOptions: dictionary([keys.useNewAPI: 1]), ]) - _ = try await send(req, timeout: defaultTimeoutDuration) + _ = try await send(\.codeCompleteClose, req) } nonisolated func completeDocumentation(id: Int) async throws -> CompletionDocumentation { - let req = dictionary([ - keys.request: requests.codeCompleteDocumentation, - keys.identifier: id, - ]) - - let resp = try await send(req, timeout: defaultTimeoutDuration) + let resp = try await send(\.codeCompleteDocumentation, dictionary([keys.identifier: id])) return CompletionDocumentation(resp) } nonisolated func completeDiagnostic(id: Int) async throws -> CompletionDiagnostic? { - let req = dictionary([ - keys.request: requests.codeCompleteDiagnostic, - keys.identifier: id, - ]) - let resp = try await send(req, timeout: defaultTimeoutDuration) + let resp = try await send(\.codeCompleteDiagnostic, dictionary([keys.identifier: id])) return CompletionDiagnostic(resp) } nonisolated func dependencyUpdated() async throws { - let req = dictionary([ - keys.request: requests.dependencyUpdated - ]) - _ = try await send(req, timeout: defaultTimeoutDuration) + _ = try await send(\.dependencyUpdated, dictionary([:])) } nonisolated func setPopularAPI(popular: [String], unpopular: [String]) async throws { let req = dictionary([ - keys.request: requests.codeCompleteSetPopularAPI, keys.codeCompleteOptions: dictionary([keys.useNewAPI: 1]), keys.popular: popular as [SKDRequestValue], keys.unpopular: unpopular as [SKDRequestValue], ]) - let resp = try await send(req, timeout: defaultTimeoutDuration) + let resp = try await send(\.codeCompleteSetPopularAPI, req) XCTAssertEqual(resp[keys.useNewAPI], 1) } @@ -1994,14 +1976,13 @@ fileprivate extension SourceKitD { notoriousModules: [String] ) async throws { let req = dictionary([ - keys.request: requests.codeCompleteSetPopularAPI, keys.codeCompleteOptions: dictionary([keys.useNewAPI: 1]), keys.scopedPopularityTablePath: scopedPopularityDataPath, keys.popularModules: popularModules as [SKDRequestValue], keys.notoriousModules: notoriousModules as [SKDRequestValue], ]) - let resp = try await send(req, timeout: defaultTimeoutDuration) + let resp = try await send(\.codeCompleteSetPopularAPI, req) XCTAssertEqual(resp[keys.useNewAPI], 1) } @@ -2019,13 +2000,12 @@ fileprivate extension SourceKitD { ]) } let req = dictionary([ - keys.request: requests.codeCompleteSetPopularAPI, keys.codeCompleteOptions: dictionary([keys.useNewAPI: 1]), keys.symbolPopularity: symbolPopularity as [SKDRequestValue], keys.modulePopularity: modulePopularity as [SKDRequestValue], ]) - let resp = try await send(req, timeout: defaultTimeoutDuration) + let resp = try await send(\.codeCompleteSetPopularAPI, req) XCTAssertEqual(resp[keys.useNewAPI], 1) } From da90f34e1005812746bca4b59bd9242923d7a498 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Thu, 27 Mar 2025 11:00:08 -0700 Subject: [PATCH 02/13] Add contextual request support to `sourcekit-lsp diagnose` Teach `sourcekit-lsp diagnose` how to extract contextual requests from the system log and use them to reduce sourcekitd crashes. --- Sources/Diagnose/CMakeLists.txt | 1 + Sources/Diagnose/DiagnoseCommand.swift | 4 + Sources/Diagnose/MergeSwiftFiles.swift | 1 + Sources/Diagnose/OSLogScraper.swift | 70 ++++++++- Sources/Diagnose/ReduceCommand.swift | 12 +- Sources/Diagnose/ReduceFrontendCommand.swift | 1 + Sources/Diagnose/ReproducerBundle.swift | 14 +- Sources/Diagnose/RequestInfo.swift | 135 ++++++++++++------ .../RunSourcekitdRequestCommand.swift | 67 +++++++-- .../Diagnose/SourceKitDRequestExecutor.swift | 63 +++++--- Sources/Diagnose/SourceReducer.swift | 2 + Sources/Diagnose/Toolchain+PluginPaths.swift | 23 +++ Sources/SKUtilities/CMakeLists.txt | 2 +- Tests/DiagnoseTests/DiagnoseTests.swift | 14 +- 14 files changed, 314 insertions(+), 95 deletions(-) create mode 100644 Sources/Diagnose/Toolchain+PluginPaths.swift diff --git a/Sources/Diagnose/CMakeLists.txt b/Sources/Diagnose/CMakeLists.txt index 6e5b7dabd..3ea6e1159 100644 --- a/Sources/Diagnose/CMakeLists.txt +++ b/Sources/Diagnose/CMakeLists.txt @@ -19,6 +19,7 @@ add_library(Diagnose STATIC StderrStreamConcurrencySafe.swift SwiftFrontendCrashScraper.swift Toolchain+SwiftFrontend.swift + Toolchain+PluginPaths.swift TraceFromSignpostsCommand.swift) set_target_properties(Diagnose PROPERTIES diff --git a/Sources/Diagnose/DiagnoseCommand.swift b/Sources/Diagnose/DiagnoseCommand.swift index 05a5dba8f..232c516c6 100644 --- a/Sources/Diagnose/DiagnoseCommand.swift +++ b/Sources/Diagnose/DiagnoseCommand.swift @@ -13,6 +13,7 @@ package import ArgumentParser import Foundation import LanguageServerProtocolExtensions +import SKLogging import SwiftExtensions import TSCExtensions import ToolchainRegistry @@ -136,6 +137,7 @@ package struct DiagnoseCommand: AsyncParsableCommand { break } catch { // Reducing this request failed. Continue reducing the next one, maybe that one succeeds. + logger.info("Reducing sourcekitd crash failed: \(error.forLogging)") } } } @@ -173,6 +175,7 @@ package struct DiagnoseCommand: AsyncParsableCommand { let executor = OutOfProcessSourceKitRequestExecutor( sourcekitd: sourcekitd, + pluginPaths: toolchain.pluginPaths, swiftFrontend: crashInfo.swiftFrontend, reproducerPredicate: nil ) @@ -457,6 +460,7 @@ package struct DiagnoseCommand: AsyncParsableCommand { let requestInfo = requestInfo let executor = OutOfProcessSourceKitRequestExecutor( sourcekitd: sourcekitd, + pluginPaths: toolchain.pluginPaths, swiftFrontend: swiftFrontend, reproducerPredicate: nil ) diff --git a/Sources/Diagnose/MergeSwiftFiles.swift b/Sources/Diagnose/MergeSwiftFiles.swift index e204008b5..1cfa401cb 100644 --- a/Sources/Diagnose/MergeSwiftFiles.swift +++ b/Sources/Diagnose/MergeSwiftFiles.swift @@ -31,6 +31,7 @@ extension RequestInfo { let compilerArgs = compilerArgs.filter { $0 != "-primary-file" && !$0.hasSuffix(".swift") } + ["$FILE"] let mergedRequestInfo = RequestInfo( requestTemplate: requestTemplate, + contextualRequestTemplates: contextualRequestTemplates, offset: offset, compilerArgs: compilerArgs, fileContents: mergedFile diff --git a/Sources/Diagnose/OSLogScraper.swift b/Sources/Diagnose/OSLogScraper.swift index 776d27e8a..287c79e5b 100644 --- a/Sources/Diagnose/OSLogScraper.swift +++ b/Sources/Diagnose/OSLogScraper.swift @@ -12,6 +12,8 @@ #if canImport(OSLog) import OSLog +import SKLogging +import RegexBuilder /// Reads oslog messages to find recent sourcekitd crashes. struct OSLogScraper { @@ -45,34 +47,90 @@ struct OSLogScraper { #"subsystem CONTAINS "sourcekit-lsp" AND composedMessage CONTAINS "sourcekitd crashed" AND category = %@"#, logCategory ) - var isInFileContentSection = false + enum LogSection { + case request + case fileContents + case contextualRequest + } + var section = LogSection.request var request = "" var fileContents = "" + var contextualRequests: [String] = [] + let sourcekitdCrashedRegex = Regex { + "sourcekitd crashed (" + OneOrMore(.digit) + "/" + OneOrMore(.digit) + ")" + } + let contextualRequestRegex = Regex { + "Contextual request " + OneOrMore(.digit) + " / " + OneOrMore(.digit) + ":" + } + for entry in try getLogEntries(matching: predicate) { for line in entry.composedMessage.components(separatedBy: "\n") { - if line.starts(with: "sourcekitd crashed (") { + if try sourcekitdCrashedRegex.wholeMatch(in: line) != nil { continue } if line == "Request:" { continue } if line == "File contents:" { - isInFileContentSection = true + section = .fileContents + continue + } + if line == "File contents:" { + section = .fileContents + continue + } + if try contextualRequestRegex.wholeMatch(in: line) != nil { + section = .contextualRequest + contextualRequests.append("") continue } if line == "--- End Chunk" { continue } - if isInFileContentSection { - fileContents += line + "\n" - } else { + switch section { + case .request: request += line + "\n" + case .fileContents: + fileContents += line + "\n" + case .contextualRequest: + if !contextualRequests.isEmpty { + contextualRequests[contextualRequests.count - 1] += line + "\n" + } else { + // Should never happen because we have appended at least one element to `contextualRequests` when switching + // to the `contextualRequest` section. + logger.fault("Dropping contextual request line: \(line)") + } } } } var requestInfo = try RequestInfo(request: request) + + let contextualRequestInfos = contextualRequests.compactMap { contextualRequest in + orLog("Processsing contextual request") { + try RequestInfo(request: contextualRequest) + } + }.filter { contextualRequest in + if contextualRequest.fileContents != requestInfo.fileContents { + logger.error("Contextual request concerns a different file than the crashed request. Ignoring it") + return false + } + return true + } + requestInfo.contextualRequestTemplates = contextualRequestInfos.map(\.requestTemplate) + if requestInfo.compilerArgs.isEmpty { + requestInfo.compilerArgs = contextualRequestInfos.last(where: { !$0.compilerArgs.isEmpty })?.compilerArgs ?? [] + } requestInfo.fileContents = fileContents + return requestInfo } diff --git a/Sources/Diagnose/ReduceCommand.swift b/Sources/Diagnose/ReduceCommand.swift index fb3788725..80ce597cc 100644 --- a/Sources/Diagnose/ReduceCommand.swift +++ b/Sources/Diagnose/ReduceCommand.swift @@ -12,6 +12,7 @@ package import ArgumentParser import Foundation +import SourceKitD import ToolchainRegistry import struct TSCBasic.AbsolutePath @@ -68,12 +69,16 @@ package struct ReduceCommand: AsyncParsableCommand { @MainActor package func run() async throws { - guard let sourcekitd = try await toolchain?.sourcekitd else { + guard let toolchain = try await toolchain else { + throw GenericError("Unable to find toolchain") + } + guard let sourcekitd = toolchain.sourcekitd else { throw GenericError("Unable to find sourcekitd.framework") } - guard let swiftFrontend = try await toolchain?.swiftFrontend else { + guard let swiftFrontend = toolchain.swiftFrontend else { throw GenericError("Unable to find sourcekitd.framework") } + let pluginPaths = toolchain.pluginPaths let progressBar = PercentProgressAnimation(stream: stderrStreamConcurrencySafe, header: "Reducing sourcekitd issue") @@ -82,6 +87,7 @@ package struct ReduceCommand: AsyncParsableCommand { let executor = OutOfProcessSourceKitRequestExecutor( sourcekitd: sourcekitd, + pluginPaths: pluginPaths, swiftFrontend: swiftFrontend, reproducerPredicate: nsPredicate ) @@ -96,6 +102,6 @@ package struct ReduceCommand: AsyncParsableCommand { try reduceRequestInfo.fileContents.write(to: reducedSourceFile, atomically: true, encoding: .utf8) print("Reduced Request:") - print(try reduceRequestInfo.request(for: reducedSourceFile)) + print(try reduceRequestInfo.requests(for: reducedSourceFile).joined(separator: "\n\n\n\n")) } } diff --git a/Sources/Diagnose/ReduceFrontendCommand.swift b/Sources/Diagnose/ReduceFrontendCommand.swift index 2d7c5316f..a779237f7 100644 --- a/Sources/Diagnose/ReduceFrontendCommand.swift +++ b/Sources/Diagnose/ReduceFrontendCommand.swift @@ -90,6 +90,7 @@ package struct ReduceFrontendCommand: AsyncParsableCommand { let executor = OutOfProcessSourceKitRequestExecutor( sourcekitd: sourcekitd, + pluginPaths: nil, swiftFrontend: swiftFrontend, reproducerPredicate: nsPredicate ) diff --git a/Sources/Diagnose/ReproducerBundle.swift b/Sources/Diagnose/ReproducerBundle.swift index 3ff842c8a..8fdccb917 100644 --- a/Sources/Diagnose/ReproducerBundle.swift +++ b/Sources/Diagnose/ReproducerBundle.swift @@ -40,12 +40,14 @@ func makeReproducerBundle(for requestInfo: RequestInfo, toolchain: Toolchain, bu + requestInfo.compilerArgs.replacing(["$FILE"], with: ["./input.swift"]).joined(separator: " \\\n") try command.write(to: bundlePath.appendingPathComponent("command.sh"), atomically: true, encoding: .utf8) } else { - let request = try requestInfo.request(for: URL(fileURLWithPath: "/input.swift")) - try request.write( - to: bundlePath.appendingPathComponent("request.yml"), - atomically: true, - encoding: .utf8 - ) + let requests = try requestInfo.requests(for: bundlePath.appendingPathComponent("input.swift")) + for (index, request) in requests.enumerated() { + try request.write( + to: bundlePath.appendingPathComponent("request-\(index).yml"), + atomically: true, + encoding: .utf8 + ) + } } for compilerArg in requestInfo.compilerArgs { // Find the first slash so we are also able to copy files from eg. diff --git a/Sources/Diagnose/RequestInfo.swift b/Sources/Diagnose/RequestInfo.swift index 696a07f13..8bba0b193 100644 --- a/Sources/Diagnose/RequestInfo.swift +++ b/Sources/Diagnose/RequestInfo.swift @@ -17,11 +17,19 @@ import SwiftExtensions /// All the information necessary to replay a sourcektid request. package struct RequestInfo: Sendable { /// The JSON request object. Contains the following dynamic placeholders: - /// - `$OFFSET`: To be replaced by `offset` before running the request - /// - `$FILE`: Will be replaced with a path to the file that contains the reduced source code. /// - `$COMPILER_ARGS`: Will be replaced by the compiler arguments of the request + /// - `$FILE`: Will be replaced with a path to the file that contains the reduced source code. + /// - `$FILE_CONTENTS`: Will be replaced by the contents of the reduced source file inside quotes + /// - `$OFFSET`: To be replaced by `offset` before running the request var requestTemplate: String + /// Requests that should be executed before `requestTemplate` to set up state in sourcekitd so that `requestTemplate` + /// can reproduce an issue, eg. sending an `editor.open` before a `codecomplete.open` so that we have registered the + /// compiler arguments in the SourceKit plugin. + /// + /// These request templates receive the same substitutions as `requestTemplate`. + var contextualRequestTemplates: [String] + /// The offset at which the sourcekitd request should be run. Replaces the /// `$OFFSET` placeholder in the request template. var offset: Int @@ -32,7 +40,7 @@ package struct RequestInfo: Sendable { /// The contents of the file that the sourcekitd request operates on. package var fileContents: String - package func request(for file: URL) throws -> String { + package func requests(for file: URL) throws -> [String] { let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes] guard var compilerArgs = String(data: try encoder.encode(compilerArgs), encoding: .utf8) else { @@ -40,11 +48,15 @@ package struct RequestInfo: Sendable { } // Drop the opening `[` and `]`. The request template already contains them compilerArgs = String(compilerArgs.dropFirst().dropLast()) - return + let quotedFileContents = + try String(data: JSONEncoder().encode(try String(contentsOf: file, encoding: .utf8)), encoding: .utf8) ?? "" + return try (contextualRequestTemplates + [requestTemplate]).map { requestTemplate in requestTemplate - .replacingOccurrences(of: "$OFFSET", with: String(offset)) - .replacingOccurrences(of: "$COMPILER_ARGS", with: compilerArgs) - .replacingOccurrences(of: "$FILE", with: try file.filePath.replacing(#"\"#, with: #"\\"#)) + .replacingOccurrences(of: "$OFFSET", with: String(offset)) + .replacingOccurrences(of: "$COMPILER_ARGS", with: compilerArgs) + .replacingOccurrences(of: "$FILE_CONTENTS", with: quotedFileContents) + .replacingOccurrences(of: "$FILE", with: try file.filePath.replacing(#"\"#, with: #"\\"#)) + } } /// A fake value that is used to indicate that we are reducing a `swift-frontend` issue instead of a sourcekitd issue. @@ -57,8 +69,15 @@ package struct RequestInfo: Sendable { } """ - package init(requestTemplate: String, offset: Int, compilerArgs: [String], fileContents: String) { + package init( + requestTemplate: String, + contextualRequestTemplates: [String], + offset: Int, + compilerArgs: [String], + fileContents: String + ) { self.requestTemplate = requestTemplate + self.contextualRequestTemplates = contextualRequestTemplates self.offset = offset self.compilerArgs = compilerArgs self.fileContents = fileContents @@ -70,42 +89,19 @@ package struct RequestInfo: Sendable { package init(request: String) throws { var requestTemplate = request - // Extract offset - let offsetRegex = Regex { - "key.offset: " - Capture(ZeroOrMore(.digit)) - } - if let offsetMatch = requestTemplate.matches(of: offsetRegex).only { - offset = Int(offsetMatch.1)! - requestTemplate.replace(offsetRegex, with: "key.offset: $OFFSET") - } else { - offset = 0 - } - // If the request contained source text, remove it. We want to pick it up from the file on disk and most (possibly // all) sourcekitd requests use key.sourcefile if key.sourcetext is missing. - requestTemplate.replace(#/ *key.sourcetext: .*\n/#, with: "") + requestTemplate.replace(#/ *key.sourcetext: .*\n/#, with: #"key.sourcetext: $FILE_CONTENTS\#n"#) - // Extract source file - let sourceFileRegex = Regex { - #"key.sourcefile: ""# - Capture(ZeroOrMore(#/[^"]/#)) - "\"" - } - guard let sourceFileMatch = requestTemplate.matches(of: sourceFileRegex).only else { - throw GenericError("Failed to find key.sourcefile in the request") - } - let sourceFilePath = String(sourceFileMatch.1) - requestTemplate.replace(sourceFileMatch.1, with: "$FILE") - - // Extract compiler arguments - let compilerArgsExtraction = try extractCompilerArguments(from: requestTemplate) - requestTemplate = compilerArgsExtraction.template - compilerArgs = compilerArgsExtraction.compilerArgs + let sourceFilePath: URL + (requestTemplate, offset) = try extractOffset(from: requestTemplate) + (requestTemplate, sourceFilePath) = try extractSourceFile(from: requestTemplate) + (requestTemplate, compilerArgs) = try extractCompilerArguments(from: requestTemplate) self.requestTemplate = requestTemplate + self.contextualRequestTemplates = [] - fileContents = try String(contentsOf: URL(fileURLWithPath: sourceFilePath), encoding: .utf8) + fileContents = try String(contentsOf: sourceFilePath, encoding: .utf8) } /// Create a `RequestInfo` that is used to reduce a `swift-frontend issue` @@ -139,6 +135,7 @@ package struct RequestInfo: Sendable { // `mergeSwiftFiles`. self.init( requestTemplate: Self.fakeRequestTemplateForFrontendIssues, + contextualRequestTemplates: [], offset: 0, compilerArgs: frontendArgsWithFilelistInlined, fileContents: "" @@ -146,6 +143,63 @@ package struct RequestInfo: Sendable { } } +private func extractOffset(from requestTemplate: String) throws -> (template: String, offset: Int) { + let offsetRegex = Regex { + "key.offset: " + Capture(ZeroOrMore(.digit)) + } + guard let offsetMatch = requestTemplate.matches(of: offsetRegex).only else { + return (requestTemplate, 0) + } + let requestTemplate = requestTemplate.replacing(offsetRegex, with: "key.offset: $OFFSET") + return (requestTemplate, Int(offsetMatch.1)!) +} + +private func extractSourceFile(from requestTemplate: String) throws -> (template: String, sourceFile: URL) { + var requestTemplate = requestTemplate + let sourceFileRegex = Regex { + #"key.sourcefile: ""# + Capture(ZeroOrMore(#/[^"]/#)) + "\"" + } + let nameRegex = Regex { + #"key.name: ""# + Capture(ZeroOrMore(#/[^"]/#)) + "\"" + } + let sourceFileMatch = requestTemplate.matches(of: sourceFileRegex).only + let nameMatch = requestTemplate.matches(of: nameRegex).only + + let sourceFilePath: String? + if let sourceFileMatch { + sourceFilePath = String(sourceFileMatch.1) + requestTemplate.replace(sourceFileMatch.1, with: "$FILE") + } else { + sourceFilePath = nil + } + + let namePath: String? + if let nameMatch { + namePath = String(nameMatch.1) + requestTemplate.replace(nameMatch.1, with: "$FILE") + } else { + namePath = nil + } + switch (sourceFilePath, namePath) { + case (let sourceFilePath?, let namePath?): + if sourceFilePath != namePath { + throw GenericError("Mismatching find key.sourcefile and key.name in the request") + } + return (requestTemplate, URL(fileURLWithPath: sourceFilePath)) + case (let sourceFilePath?, nil): + return (requestTemplate, URL(fileURLWithPath: sourceFilePath)) + case (nil, let namePath?): + return (requestTemplate, URL(fileURLWithPath: namePath)) + case (nil, nil): + throw GenericError("Failed to find key.sourcefile or key.name in the request") + } +} + private func extractCompilerArguments( from requestTemplate: String ) throws -> (template: String, compilerArgs: [String]) { @@ -160,9 +214,6 @@ private func extractCompilerArguments( } let template = lines[...compilerArgsStartIndex] + ["$COMPILER_ARGS"] + lines[compilerArgsEndIndex...] let compilerArgsJson = "[" + lines[(compilerArgsStartIndex + 1).. SourceKitDRequestResult { + var arguments = [ + ProcessInfo.processInfo.arguments[0], + "debug", + "run-sourcekitd-request", + "--sourcekitd", + try sourcekitd.filePath, + ] + if let pluginPaths { + arguments += [ + "--sourcekit-plugin-path", + try pluginPaths.servicePlugin.filePath, + "--sourcekit-client-plugin-path", + try pluginPaths.clientPlugin.filePath, + ] + } + try request.fileContents.write(to: temporarySourceFile, atomically: true, encoding: .utf8) - let requestString = try request.request(for: temporarySourceFile) - try requestString.write(to: temporaryRequestFile, atomically: true, encoding: .utf8) - - let process = Process( - arguments: [ - ProcessInfo.processInfo.arguments[0], - "debug", - "run-sourcekitd-request", - "--sourcekitd", - try sourcekitd.filePath, + let requestStrings = try request.requests(for: temporarySourceFile) + for (index, requestString) in requestStrings.enumerated() { + let temporaryRequestFile = temporaryDirectory.appendingPathComponent("request-\(index).yml") + try requestString.write( + to: temporaryRequestFile, + atomically: true, + encoding: .utf8 + ) + arguments += [ "--request-file", try temporaryRequestFile.filePath, ] - ) - try process.launch() - let result = try await process.waitUntilExit() + } + let result = try await Process.run(arguments: arguments, workingDirectory: nil) return requestResult(for: result) } } diff --git a/Sources/Diagnose/SourceReducer.swift b/Sources/Diagnose/SourceReducer.swift index aed94b59e..017784dbc 100644 --- a/Sources/Diagnose/SourceReducer.swift +++ b/Sources/Diagnose/SourceReducer.swift @@ -269,6 +269,7 @@ fileprivate class SourceReducer { let reducedRequestInfo = RequestInfo( requestTemplate: requestInfo.requestTemplate, + contextualRequestTemplates: requestInfo.contextualRequestTemplates, offset: adjustedOffset, compilerArgs: requestInfo.compilerArgs, fileContents: reducedSource @@ -632,6 +633,7 @@ fileprivate func getSwiftInterface( """ let requestInfo = RequestInfo( requestTemplate: requestTemplate, + contextualRequestTemplates: [], offset: 0, compilerArgs: compilerArgs, fileContents: "" diff --git a/Sources/Diagnose/Toolchain+PluginPaths.swift b/Sources/Diagnose/Toolchain+PluginPaths.swift new file mode 100644 index 000000000..bcf673f17 --- /dev/null +++ b/Sources/Diagnose/Toolchain+PluginPaths.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 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 SourceKitD +import ToolchainRegistry + +extension Toolchain { + var pluginPaths: PluginPaths? { + guard let sourceKitClientPlugin, let sourceKitServicePlugin else { + return nil + } + return PluginPaths(clientPlugin: sourceKitClientPlugin, servicePlugin: sourceKitServicePlugin) + } +} diff --git a/Sources/SKUtilities/CMakeLists.txt b/Sources/SKUtilities/CMakeLists.txt index 70eec825b..8c32531cb 100644 --- a/Sources/SKUtilities/CMakeLists.txt +++ b/Sources/SKUtilities/CMakeLists.txt @@ -24,4 +24,4 @@ target_compile_options(SKUtilitiesForPlugin PRIVATE target_link_libraries(SKUtilitiesForPlugin PRIVATE SKLoggingForPlugin SwiftExtensionsForPlugin - $<$>:Foundation>) \ No newline at end of file + $<$>:Foundation>) diff --git a/Tests/DiagnoseTests/DiagnoseTests.swift b/Tests/DiagnoseTests/DiagnoseTests.swift index 9e7522250..2650faa17 100644 --- a/Tests/DiagnoseTests/DiagnoseTests.swift +++ b/Tests/DiagnoseTests/DiagnoseTests.swift @@ -307,6 +307,7 @@ private class InProcessSourceKitRequestExecutor: SourceKitRequestExecutor { func runSwiftFrontend(request: RequestInfo) async throws -> SourceKitDRequestResult { return try await OutOfProcessSourceKitRequestExecutor( sourcekitd: sourcekitd, + pluginPaths: sourceKitPluginPaths, swiftFrontend: swiftFrontend, reproducerPredicate: reproducerPredicate ).runSwiftFrontend(request: request) @@ -314,14 +315,21 @@ private class InProcessSourceKitRequestExecutor: SourceKitRequestExecutor { func runSourceKitD(request: RequestInfo) async throws -> SourceKitDRequestResult { try request.fileContents.write(to: temporarySourceFile, atomically: true, encoding: .utf8) - let requestString = try request.request(for: temporarySourceFile) - logger.info("Sending request: \(requestString)") + let requestStrings = try request.requests(for: temporarySourceFile) + logger.info("Sending request: \(requestStrings.joined(separator: "\n\n\n\n"))") let sourcekitd = try await SourceKitD.getOrCreate( dylibPath: sourcekitd, pluginPaths: sourceKitPluginPaths ) - let response = try await sourcekitd.run(requestYaml: requestString) + var response: SKDResponse? = nil + for requestString in requestStrings { + response = try await sourcekitd.run(requestYaml: requestString) + } + guard let response else { + logger.error("No request executed") + return .error + } logger.info("Received response: \(response.description)") From d63419de73b44b0a2396e5041a795e9b1aee160b Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Mon, 12 May 2025 15:48:45 +0200 Subject: [PATCH 03/13] Address review comments --- Sources/Diagnose/RequestInfo.swift | 4 +--- Sources/SourceKitD/SourceKitD.swift | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Diagnose/RequestInfo.swift b/Sources/Diagnose/RequestInfo.swift index 8bba0b193..60885fd60 100644 --- a/Sources/Diagnose/RequestInfo.swift +++ b/Sources/Diagnose/RequestInfo.swift @@ -89,8 +89,6 @@ package struct RequestInfo: Sendable { package init(request: String) throws { var requestTemplate = request - // If the request contained source text, remove it. We want to pick it up from the file on disk and most (possibly - // all) sourcekitd requests use key.sourcefile if key.sourcetext is missing. requestTemplate.replace(#/ *key.sourcetext: .*\n/#, with: #"key.sourcetext: $FILE_CONTENTS\#n"#) let sourceFilePath: URL @@ -188,7 +186,7 @@ private func extractSourceFile(from requestTemplate: String) throws -> (template switch (sourceFilePath, namePath) { case (let sourceFilePath?, let namePath?): if sourceFilePath != namePath { - throw GenericError("Mismatching find key.sourcefile and key.name in the request") + throw GenericError("Mismatching key.sourcefile and key.name in the request: \(sourceFilePath) vs. \(namePath)") } return (requestTemplate, URL(fileURLWithPath: sourceFilePath)) case (let sourceFilePath?, nil): diff --git a/Sources/SourceKitD/SourceKitD.swift b/Sources/SourceKitD/SourceKitD.swift index a296946e9..ac1bfaff0 100644 --- a/Sources/SourceKitD/SourceKitD.swift +++ b/Sources/SourceKitD/SourceKitD.swift @@ -345,6 +345,8 @@ package actor SourceKitD { case requests.codeCompleteClose: contextualRequests[documentUrl, default: []].removeAll(where: { $0.kind == .codeCompleteOpen }) if contextualRequests[documentUrl]?.isEmpty ?? false { + // This should never happen because we should still have an active `.editorOpen` contextual request but just be + // safe in case we don't. contextualRequests[documentUrl] = nil } default: From 385f8ae79f2edbba9b921973b9c1d88bec8f5cb7 Mon Sep 17 00:00:00 2001 From: Ben Barham Date: Mon, 12 May 2025 18:16:57 -0700 Subject: [PATCH 04/13] Resolve paths passed in the SwiftPM configuration to the project root Resolves #2148. --- .../SwiftPMBuildSystem.swift | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift index 7273707a2..839da5883 100644 --- a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift +++ b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift @@ -203,18 +203,23 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { throw Error.cannotDetermineHostToolchain } + var absProjectRoot = try AbsolutePath(validating: projectRoot.filePath) let hostSDK = try SwiftSDK.hostSwiftSDK(AbsolutePath(validating: destinationToolchainBinDir.filePath)) let hostSwiftPMToolchain = try UserToolchain(swiftSDK: hostSDK) let destinationSDK = try SwiftSDK.deriveTargetSwiftSDK( hostSwiftSDK: hostSDK, hostTriple: hostSwiftPMToolchain.targetTriple, - customToolsets: options.swiftPMOrDefault.toolsets?.map { try AbsolutePath(validating: $0) } ?? [], + customToolsets: options.swiftPMOrDefault.toolsets?.map { + try AbsolutePath(validating: $0, relativeTo: absProjectRoot) + } ?? [], customCompileTriple: options.swiftPMOrDefault.triple.map { try Triple($0) }, swiftSDKSelector: options.swiftPMOrDefault.swiftSDK, store: SwiftSDKBundleStore( swiftSDKsDirectory: localFileSystem.getSharedSwiftSDKsDirectory( - explicitDirectory: options.swiftPMOrDefault.swiftSDKsDirectory.map { try AbsolutePath(validating: $0) } + explicitDirectory: options.swiftPMOrDefault.swiftSDKsDirectory.map { + try AbsolutePath(validating: $0, relativeTo: absProjectRoot) + } ), fileSystem: localFileSystem, observabilityScope: observabilitySystem.topScope.makeChildScope(description: "SwiftPM Bundle Store"), @@ -227,20 +232,14 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { let destinationSwiftPMToolchain = try UserToolchain(swiftSDK: destinationSDK) var location = try Workspace.Location( - forRootPackage: try AbsolutePath(validating: projectRoot.filePath), + forRootPackage: absProjectRoot, fileSystem: localFileSystem ) + if options.backgroundIndexingOrDefault { - location.scratchDirectory = try AbsolutePath( - validating: projectRoot.appendingPathComponent(".build").appendingPathComponent("index-build").filePath - ) - } else if let scratchDirectory = options.swiftPMOrDefault.scratchPath, - let scratchDirectoryPath = try? AbsolutePath( - validating: scratchDirectory, - relativeTo: AbsolutePath(validating: projectRoot.filePath) - ) - { - location.scratchDirectory = scratchDirectoryPath + location.scratchDirectory = absProjectRoot.appending(components: ".build", "index-build") + } else if let scratchDirectory = options.swiftPMOrDefault.scratchPath { + location.scratchDirectory = try AbsolutePath(validating: scratchDirectory, relativeTo: absProjectRoot) } var configuration = WorkspaceConfiguration.default From 6e09a215a0ed0253351710636b75253722017417 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Wed, 7 May 2025 11:04:30 +0200 Subject: [PATCH 05/13] Log the path that SourceKit-LSP was launched from Generally helpful to get a hint about whether an open source toolchain is used and to help with toolchain discovery issues. --- Sources/sourcekit-lsp/SourceKitLSP.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/sourcekit-lsp/SourceKitLSP.swift b/Sources/sourcekit-lsp/SourceKitLSP.swift index ec7d4ca34..15c0b1430 100644 --- a/Sources/sourcekit-lsp/SourceKitLSP.swift +++ b/Sources/sourcekit-lsp/SourceKitLSP.swift @@ -237,6 +237,8 @@ struct SourceKitLSP: AsyncParsableCommand { fatalError("failed to redirect stdout -> stderr: \(strerror(errno)!)") } + logger.log("sourcekit-lsp launched from \(ProcessInfo.processInfo.arguments.first ?? "")") + let globalConfigurationOptions = globalConfigurationOptions if let logLevelStr = globalConfigurationOptions.loggingOrDefault.level, let logLevel = NonDarwinLogLevel(logLevelStr) From 47a940d8bdae5fe03454bf2d1ef2750f76e0e1f2 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Tue, 13 May 2025 17:24:45 +0200 Subject: [PATCH 06/13] Fix race condition that causes task cancellation to be missed in `TaskScheduler` A queued task might have been cancelled after the execution ask was started but before the task was yielded to `executionTaskCreatedContinuation`. In that case the result task will simply cancel the await on the `executionTaskCreatedStream` and hence not call `valuePropagatingCancellation` on the execution task. This means that the queued task cancellation wouldn't be propagated to the execution task. To address this, check if `resultTaskCancelled` was set and, if so, explicitly cancel the execution task here. Fixes an issue I saw in CI during PR testing. --- Sources/SemanticIndex/PreparationTaskDescription.swift | 4 +--- Sources/SemanticIndex/TaskScheduler.swift | 8 ++++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Sources/SemanticIndex/PreparationTaskDescription.swift b/Sources/SemanticIndex/PreparationTaskDescription.swift index 04626cffc..841c81bf1 100644 --- a/Sources/SemanticIndex/PreparationTaskDescription.swift +++ b/Sources/SemanticIndex/PreparationTaskDescription.swift @@ -105,9 +105,7 @@ package struct PreparationTaskDescription: IndexTaskDescription { do { try await buildSystemManager.prepare(targets: Set(targetsToPrepare)) } catch { - logger.error( - "Preparation failed: \(error.forLogging)" - ) + logger.error("Preparation failed: \(error.forLogging)") } await hooks.preparationTaskDidFinish?(self) if !Task.isCancelled { diff --git a/Sources/SemanticIndex/TaskScheduler.swift b/Sources/SemanticIndex/TaskScheduler.swift index 95060f71f..45b1de1bb 100644 --- a/Sources/SemanticIndex/TaskScheduler.swift +++ b/Sources/SemanticIndex/TaskScheduler.swift @@ -249,6 +249,14 @@ package actor QueuedTask { _isExecuting.value = true executionTask = task executionTaskCreatedContinuation.yield(task) + if self.resultTaskCancelled.value { + // The queued task might have been cancelled after the execution ask was started but before the task was yielded + // to `executionTaskCreatedContinuation`. In that case the result task will simply cancel the await on the + // `executionTaskCreatedStream` and hence not call `valuePropagatingCancellation` on the execution task. This + // means that the queued task cancellation wouldn't be propagated to the execution task. To address this, check if + // `resultTaskCancelled` was set and, if so, explicitly cancel the execution task here. + task.cancel() + } await executionStateChangedCallback?(self, .executing) return await task.value } From ba1fb5de070e698154faf16e87b83f7935681aae Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Tue, 13 May 2025 17:28:24 +0200 Subject: [PATCH 07/13] Do not schedule any new tasks in `TaskScheduler` if it has been shut down This is what `shutDown()` is documented to do. I also remember having this check before, it might have gotten lost during a rebase when I was working on https://github.com/swiftlang/sourcekit-lsp/pull/2081. I noticed this while investigating https://github.com/swiftlang/sourcekit-lsp/pull/2152: In this case `buildTarget/prepare` was cancelled because the SourceKit-LSP server was shut down but indexing of a file was still started after the shutdown now that preparation had finished (because it was cancelled). --- Sources/SemanticIndex/TaskScheduler.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/SemanticIndex/TaskScheduler.swift b/Sources/SemanticIndex/TaskScheduler.swift index 95060f71f..06323c491 100644 --- a/Sources/SemanticIndex/TaskScheduler.swift +++ b/Sources/SemanticIndex/TaskScheduler.swift @@ -513,6 +513,9 @@ package actor TaskScheduler { /// /// This will continue calling itself until the queue is empty. private func poke() { + if isShutDown { + return + } pendingTasks.sort(by: { $0.priority > $1.priority }) for task in pendingTasks { guard From 8797f9ca85cb96e03dd9242dd3cda42e41f8b651 Mon Sep 17 00:00:00 2001 From: Wilfred Hughes Date: Tue, 13 May 2025 16:00:26 +0100 Subject: [PATCH 08/13] Fix Package.swift warnings Previously Package.swift referred a non-existent CMakeLists.txt file and used old URLs for the swift-tools-support-core repository, leading to the following build warnings: warning: 'sourcekit-lsp': Invalid Exclude '/Users/wilfred/src/sourcekit-lsp/Sources/DocCDocumentation/CMakeLists.txt': File not found. warning: 'swift-package-manager': 'swift-package-manager' dependency on 'https://github.com/swiftlang/swift-tools-support-core.git' conflicts with dependency on 'https://github.com/apple/swift-tools-support-core.git' which has the same identity 'swift-tools-support-core'. this will be escalated to an error in future versions of SwiftPM. warning: 'swift-driver': 'swift-driver' dependency on 'https://github.com/swiftlang/swift-tools-support-core.git' conflicts with dependency on 'https://github.com/apple/swift-tools-support-core.git' which has the same identity 'swift-tools-support-core'. this will be escalated to an error in future versions of SwiftPM. --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index e0c2e0f3f..fc4cf6a53 100644 --- a/Package.swift +++ b/Package.swift @@ -229,7 +229,7 @@ var targets: [Target] = [ .product(name: "SwiftDocC", package: "swift-docc"), .product(name: "SymbolKit", package: "swift-docc-symbolkit"), ], - exclude: ["CMakeLists.txt"], + exclude: [], swiftSettings: globalSwiftSettings ), @@ -807,7 +807,7 @@ var dependencies: [Package.Dependency] { .package(url: "https://github.com/swiftlang/swift-docc.git", branch: relatedDependenciesBranch), .package(url: "https://github.com/swiftlang/swift-docc-symbolkit.git", branch: relatedDependenciesBranch), .package(url: "https://github.com/swiftlang/swift-markdown.git", branch: relatedDependenciesBranch), - .package(url: "https://github.com/apple/swift-tools-support-core.git", branch: relatedDependenciesBranch), + .package(url: "https://github.com/swiftlang/swift-tools-support-core.git", branch: relatedDependenciesBranch), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.4.0"), .package(url: "https://github.com/swiftlang/swift-syntax.git", branch: relatedDependenciesBranch), .package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"), From f65bb76afe431e48a1856bfcf63958dda895ac80 Mon Sep 17 00:00:00 2001 From: Ben Barham Date: Wed, 14 May 2025 10:22:44 -0700 Subject: [PATCH 09/13] Add more code owners --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cf1506438..bdb02647e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,4 +8,4 @@ # Order is important. The last matching pattern has the most precedence. # Owner of anything in SourceKit-LSP not owned by anyone else. -* @ahoppen +* @ahoppen @bnbarham @hamishknight @rintaro From a9094391f0b7dd0a8fd5aa0ec24604e9368946bd Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Wed, 14 May 2025 18:44:30 +0200 Subject: [PATCH 10/13] Pass `cancelOnSubsequentRequest: 0` to all requests that support it Adopt the option introduced by https://github.com/swiftlang/swift/pull/81507. SourceKit-LSP uses explicit cancellation and perform any implicit cancellation inside sourcekitd. Fixes #2021 rdar://145871554 --- Sources/SourceKitLSP/Swift/MacroExpansion.swift | 1 + Sources/SourceKitLSP/Swift/RefactoringResponse.swift | 1 + Sources/SourceKitLSP/Swift/VariableTypeInfo.swift | 1 + 3 files changed, 3 insertions(+) diff --git a/Sources/SourceKitLSP/Swift/MacroExpansion.swift b/Sources/SourceKitLSP/Swift/MacroExpansion.swift index 03f505fbd..74749100f 100644 --- a/Sources/SourceKitLSP/Swift/MacroExpansion.swift +++ b/Sources/SourceKitLSP/Swift/MacroExpansion.swift @@ -94,6 +94,7 @@ actor MacroExpansionManager { let length = snapshot.utf8OffsetRange(of: range).count let skreq = swiftLanguageService.sourcekitd.dictionary([ + keys.cancelOnSubsequentRequest: 0, // Preferred name for e.g. an extracted variable. // Empty string means sourcekitd chooses a name automatically. keys.name: "", diff --git a/Sources/SourceKitLSP/Swift/RefactoringResponse.swift b/Sources/SourceKitLSP/Swift/RefactoringResponse.swift index 382595b76..be497a59d 100644 --- a/Sources/SourceKitLSP/Swift/RefactoringResponse.swift +++ b/Sources/SourceKitLSP/Swift/RefactoringResponse.swift @@ -117,6 +117,7 @@ extension SwiftLanguageService { let utf8Column = snapshot.lineTable.utf8ColumnAt(line: line, utf16Column: utf16Column) let skreq = sourcekitd.dictionary([ + keys.cancelOnSubsequentRequest: 0, // Preferred name for e.g. an extracted variable. // Empty string means sourcekitd chooses a name automatically. keys.name: "", diff --git a/Sources/SourceKitLSP/Swift/VariableTypeInfo.swift b/Sources/SourceKitLSP/Swift/VariableTypeInfo.swift index a13409c53..9c0346911 100644 --- a/Sources/SourceKitLSP/Swift/VariableTypeInfo.swift +++ b/Sources/SourceKitLSP/Swift/VariableTypeInfo.swift @@ -86,6 +86,7 @@ extension SwiftLanguageService { let snapshot = try await self.latestSnapshot(for: uri) let skreq = sourcekitd.dictionary([ + keys.cancelOnSubsequentRequest: 0, keys.sourceFile: snapshot.uri.sourcekitdSourceFile, keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath, keys.compilerArgs: await self.compileCommand(for: uri, fallbackAfterTimeout: false)?.compilerArgs From 8a908f21b69b6386692e738df1edcf28566a4d81 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 15 May 2025 11:25:23 +0100 Subject: [PATCH 11/13] Pass `hostToolchainBinDir` to `SwiftSDKBundleStore` (#2157) This became a required parameter in https://github.com/swiftlang/swift-package-manager/pull/8668, which can be easily computed, since the host toolchain in practice is always available when `SwiftSDKBundleStore` is initialized. --- Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift index 839da5883..209c49d4a 100644 --- a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift +++ b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift @@ -221,6 +221,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem { try AbsolutePath(validating: $0, relativeTo: absProjectRoot) } ), + hostToolchainBinDir: hostSwiftPMToolchain.swiftCompilerPath.parentDirectory, fileSystem: localFileSystem, observabilityScope: observabilitySystem.topScope.makeChildScope(description: "SwiftPM Bundle Store"), outputHandler: { _ in } From 020ca3989e48d0b388f9d4869a2a7692f78bc8d7 Mon Sep 17 00:00:00 2001 From: Ben Barham Date: Mon, 19 May 2025 15:54:06 -0700 Subject: [PATCH 12/13] Revert "Merge pull request #2159 from bnbarham/update-codeowners" This reverts commit 66b20b9a914702cb67b5ed730ccac3df44349334, reversing changes made to 8e8016e024662339acc91fdf1a18081a19358344. Codeowners are branch managers on the release branch. --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bdb02647e..cf1506438 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,4 +8,4 @@ # Order is important. The last matching pattern has the most precedence. # Owner of anything in SourceKit-LSP not owned by anyone else. -* @ahoppen @bnbarham @hamishknight @rintaro +* @ahoppen From a0a8f1eca4a2c50a88f4c8be30418cad1af3e5e0 Mon Sep 17 00:00:00 2001 From: Ben Barham Date: Tue, 20 May 2025 15:57:42 -0700 Subject: [PATCH 13/13] Revert "Merge pull request #2094 from ahoppen/contextual-sourcekitd-request" This reverts commit 188e174cde5969e488239d0de9adb71e96377837, reversing changes made to be5ae8cf1c076734335f9c33a62eb963d5c20fb7. Should be a fairly harmless change, but it's also quite large. Skipping for 6.2. --- Package.swift | 1 - Sources/Diagnose/CMakeLists.txt | 1 - Sources/Diagnose/DiagnoseCommand.swift | 4 - Sources/Diagnose/MergeSwiftFiles.swift | 1 - Sources/Diagnose/OSLogScraper.swift | 70 +-------- Sources/Diagnose/ReduceCommand.swift | 12 +- Sources/Diagnose/ReduceFrontendCommand.swift | 1 - Sources/Diagnose/ReproducerBundle.swift | 14 +- Sources/Diagnose/RequestInfo.swift | 137 ++++++------------ .../RunSourcekitdRequestCommand.swift | 67 ++------- .../Diagnose/SourceKitDRequestExecutor.swift | 63 +++----- Sources/Diagnose/SourceReducer.swift | 2 - Sources/Diagnose/Toolchain+PluginPaths.swift | 23 --- Sources/SKTestSupport/SkipUnless.swift | 4 +- Sources/SKTestSupport/SourceKitD+send.swift | 6 +- Sources/SKUtilities/CMakeLists.txt | 2 +- Sources/SourceKitD/SourceKitD.swift | 69 +-------- Sources/SourceKitLSP/Rename.swift | 9 +- .../Swift/CodeCompletionSession.swift | 27 ++-- Sources/SourceKitLSP/Swift/CursorInfo.swift | 3 +- .../Swift/DiagnosticReportManager.swift | 3 +- .../Swift/GeneratedInterfaceManager.swift | 22 ++- .../SourceKitLSP/Swift/MacroExpansion.swift | 6 +- .../Swift/RefactoringResponse.swift | 3 +- .../Swift/RelatedIdentifiers.swift | 3 +- .../SourceKitLSP/Swift/SemanticTokens.swift | 3 +- .../Swift/SwiftLanguageService.swift | 32 ++-- .../SourceKitLSP/Swift/VariableTypeInfo.swift | 3 +- ...thSnapshotFromDiskOpenedInSourcekitd.swift | 10 +- Tests/DiagnoseTests/DiagnoseTests.swift | 14 +- Tests/SourceKitDTests/SourceKitDTests.swift | 8 +- .../SwiftSourceKitPluginTests.swift | 50 +++++-- 32 files changed, 211 insertions(+), 462 deletions(-) delete mode 100644 Sources/Diagnose/Toolchain+PluginPaths.swift diff --git a/Package.swift b/Package.swift index 700e5b20a..fc4cf6a53 100644 --- a/Package.swift +++ b/Package.swift @@ -429,7 +429,6 @@ var targets: [Target] = [ dependencies: [ "BuildSystemIntegration", "CSKTestSupport", - "Csourcekitd", "InProcessClient", "LanguageServerProtocol", "LanguageServerProtocolExtensions", diff --git a/Sources/Diagnose/CMakeLists.txt b/Sources/Diagnose/CMakeLists.txt index 3ea6e1159..6e5b7dabd 100644 --- a/Sources/Diagnose/CMakeLists.txt +++ b/Sources/Diagnose/CMakeLists.txt @@ -19,7 +19,6 @@ add_library(Diagnose STATIC StderrStreamConcurrencySafe.swift SwiftFrontendCrashScraper.swift Toolchain+SwiftFrontend.swift - Toolchain+PluginPaths.swift TraceFromSignpostsCommand.swift) set_target_properties(Diagnose PROPERTIES diff --git a/Sources/Diagnose/DiagnoseCommand.swift b/Sources/Diagnose/DiagnoseCommand.swift index 232c516c6..05a5dba8f 100644 --- a/Sources/Diagnose/DiagnoseCommand.swift +++ b/Sources/Diagnose/DiagnoseCommand.swift @@ -13,7 +13,6 @@ package import ArgumentParser import Foundation import LanguageServerProtocolExtensions -import SKLogging import SwiftExtensions import TSCExtensions import ToolchainRegistry @@ -137,7 +136,6 @@ package struct DiagnoseCommand: AsyncParsableCommand { break } catch { // Reducing this request failed. Continue reducing the next one, maybe that one succeeds. - logger.info("Reducing sourcekitd crash failed: \(error.forLogging)") } } } @@ -175,7 +173,6 @@ package struct DiagnoseCommand: AsyncParsableCommand { let executor = OutOfProcessSourceKitRequestExecutor( sourcekitd: sourcekitd, - pluginPaths: toolchain.pluginPaths, swiftFrontend: crashInfo.swiftFrontend, reproducerPredicate: nil ) @@ -460,7 +457,6 @@ package struct DiagnoseCommand: AsyncParsableCommand { let requestInfo = requestInfo let executor = OutOfProcessSourceKitRequestExecutor( sourcekitd: sourcekitd, - pluginPaths: toolchain.pluginPaths, swiftFrontend: swiftFrontend, reproducerPredicate: nil ) diff --git a/Sources/Diagnose/MergeSwiftFiles.swift b/Sources/Diagnose/MergeSwiftFiles.swift index 1cfa401cb..e204008b5 100644 --- a/Sources/Diagnose/MergeSwiftFiles.swift +++ b/Sources/Diagnose/MergeSwiftFiles.swift @@ -31,7 +31,6 @@ extension RequestInfo { let compilerArgs = compilerArgs.filter { $0 != "-primary-file" && !$0.hasSuffix(".swift") } + ["$FILE"] let mergedRequestInfo = RequestInfo( requestTemplate: requestTemplate, - contextualRequestTemplates: contextualRequestTemplates, offset: offset, compilerArgs: compilerArgs, fileContents: mergedFile diff --git a/Sources/Diagnose/OSLogScraper.swift b/Sources/Diagnose/OSLogScraper.swift index 287c79e5b..776d27e8a 100644 --- a/Sources/Diagnose/OSLogScraper.swift +++ b/Sources/Diagnose/OSLogScraper.swift @@ -12,8 +12,6 @@ #if canImport(OSLog) import OSLog -import SKLogging -import RegexBuilder /// Reads oslog messages to find recent sourcekitd crashes. struct OSLogScraper { @@ -47,90 +45,34 @@ struct OSLogScraper { #"subsystem CONTAINS "sourcekit-lsp" AND composedMessage CONTAINS "sourcekitd crashed" AND category = %@"#, logCategory ) - enum LogSection { - case request - case fileContents - case contextualRequest - } - var section = LogSection.request + var isInFileContentSection = false var request = "" var fileContents = "" - var contextualRequests: [String] = [] - let sourcekitdCrashedRegex = Regex { - "sourcekitd crashed (" - OneOrMore(.digit) - "/" - OneOrMore(.digit) - ")" - } - let contextualRequestRegex = Regex { - "Contextual request " - OneOrMore(.digit) - " / " - OneOrMore(.digit) - ":" - } - for entry in try getLogEntries(matching: predicate) { for line in entry.composedMessage.components(separatedBy: "\n") { - if try sourcekitdCrashedRegex.wholeMatch(in: line) != nil { + if line.starts(with: "sourcekitd crashed (") { continue } if line == "Request:" { continue } if line == "File contents:" { - section = .fileContents - continue - } - if line == "File contents:" { - section = .fileContents - continue - } - if try contextualRequestRegex.wholeMatch(in: line) != nil { - section = .contextualRequest - contextualRequests.append("") + isInFileContentSection = true continue } if line == "--- End Chunk" { continue } - switch section { - case .request: - request += line + "\n" - case .fileContents: + if isInFileContentSection { fileContents += line + "\n" - case .contextualRequest: - if !contextualRequests.isEmpty { - contextualRequests[contextualRequests.count - 1] += line + "\n" - } else { - // Should never happen because we have appended at least one element to `contextualRequests` when switching - // to the `contextualRequest` section. - logger.fault("Dropping contextual request line: \(line)") - } + } else { + request += line + "\n" } } } var requestInfo = try RequestInfo(request: request) - - let contextualRequestInfos = contextualRequests.compactMap { contextualRequest in - orLog("Processsing contextual request") { - try RequestInfo(request: contextualRequest) - } - }.filter { contextualRequest in - if contextualRequest.fileContents != requestInfo.fileContents { - logger.error("Contextual request concerns a different file than the crashed request. Ignoring it") - return false - } - return true - } - requestInfo.contextualRequestTemplates = contextualRequestInfos.map(\.requestTemplate) - if requestInfo.compilerArgs.isEmpty { - requestInfo.compilerArgs = contextualRequestInfos.last(where: { !$0.compilerArgs.isEmpty })?.compilerArgs ?? [] - } requestInfo.fileContents = fileContents - return requestInfo } diff --git a/Sources/Diagnose/ReduceCommand.swift b/Sources/Diagnose/ReduceCommand.swift index 80ce597cc..fb3788725 100644 --- a/Sources/Diagnose/ReduceCommand.swift +++ b/Sources/Diagnose/ReduceCommand.swift @@ -12,7 +12,6 @@ package import ArgumentParser import Foundation -import SourceKitD import ToolchainRegistry import struct TSCBasic.AbsolutePath @@ -69,16 +68,12 @@ package struct ReduceCommand: AsyncParsableCommand { @MainActor package func run() async throws { - guard let toolchain = try await toolchain else { - throw GenericError("Unable to find toolchain") - } - guard let sourcekitd = toolchain.sourcekitd else { + guard let sourcekitd = try await toolchain?.sourcekitd else { throw GenericError("Unable to find sourcekitd.framework") } - guard let swiftFrontend = toolchain.swiftFrontend else { + guard let swiftFrontend = try await toolchain?.swiftFrontend else { throw GenericError("Unable to find sourcekitd.framework") } - let pluginPaths = toolchain.pluginPaths let progressBar = PercentProgressAnimation(stream: stderrStreamConcurrencySafe, header: "Reducing sourcekitd issue") @@ -87,7 +82,6 @@ package struct ReduceCommand: AsyncParsableCommand { let executor = OutOfProcessSourceKitRequestExecutor( sourcekitd: sourcekitd, - pluginPaths: pluginPaths, swiftFrontend: swiftFrontend, reproducerPredicate: nsPredicate ) @@ -102,6 +96,6 @@ package struct ReduceCommand: AsyncParsableCommand { try reduceRequestInfo.fileContents.write(to: reducedSourceFile, atomically: true, encoding: .utf8) print("Reduced Request:") - print(try reduceRequestInfo.requests(for: reducedSourceFile).joined(separator: "\n\n\n\n")) + print(try reduceRequestInfo.request(for: reducedSourceFile)) } } diff --git a/Sources/Diagnose/ReduceFrontendCommand.swift b/Sources/Diagnose/ReduceFrontendCommand.swift index a779237f7..2d7c5316f 100644 --- a/Sources/Diagnose/ReduceFrontendCommand.swift +++ b/Sources/Diagnose/ReduceFrontendCommand.swift @@ -90,7 +90,6 @@ package struct ReduceFrontendCommand: AsyncParsableCommand { let executor = OutOfProcessSourceKitRequestExecutor( sourcekitd: sourcekitd, - pluginPaths: nil, swiftFrontend: swiftFrontend, reproducerPredicate: nsPredicate ) diff --git a/Sources/Diagnose/ReproducerBundle.swift b/Sources/Diagnose/ReproducerBundle.swift index 8fdccb917..3ff842c8a 100644 --- a/Sources/Diagnose/ReproducerBundle.swift +++ b/Sources/Diagnose/ReproducerBundle.swift @@ -40,14 +40,12 @@ func makeReproducerBundle(for requestInfo: RequestInfo, toolchain: Toolchain, bu + requestInfo.compilerArgs.replacing(["$FILE"], with: ["./input.swift"]).joined(separator: " \\\n") try command.write(to: bundlePath.appendingPathComponent("command.sh"), atomically: true, encoding: .utf8) } else { - let requests = try requestInfo.requests(for: bundlePath.appendingPathComponent("input.swift")) - for (index, request) in requests.enumerated() { - try request.write( - to: bundlePath.appendingPathComponent("request-\(index).yml"), - atomically: true, - encoding: .utf8 - ) - } + let request = try requestInfo.request(for: URL(fileURLWithPath: "/input.swift")) + try request.write( + to: bundlePath.appendingPathComponent("request.yml"), + atomically: true, + encoding: .utf8 + ) } for compilerArg in requestInfo.compilerArgs { // Find the first slash so we are also able to copy files from eg. diff --git a/Sources/Diagnose/RequestInfo.swift b/Sources/Diagnose/RequestInfo.swift index 60885fd60..696a07f13 100644 --- a/Sources/Diagnose/RequestInfo.swift +++ b/Sources/Diagnose/RequestInfo.swift @@ -17,19 +17,11 @@ import SwiftExtensions /// All the information necessary to replay a sourcektid request. package struct RequestInfo: Sendable { /// The JSON request object. Contains the following dynamic placeholders: - /// - `$COMPILER_ARGS`: Will be replaced by the compiler arguments of the request - /// - `$FILE`: Will be replaced with a path to the file that contains the reduced source code. - /// - `$FILE_CONTENTS`: Will be replaced by the contents of the reduced source file inside quotes /// - `$OFFSET`: To be replaced by `offset` before running the request + /// - `$FILE`: Will be replaced with a path to the file that contains the reduced source code. + /// - `$COMPILER_ARGS`: Will be replaced by the compiler arguments of the request var requestTemplate: String - /// Requests that should be executed before `requestTemplate` to set up state in sourcekitd so that `requestTemplate` - /// can reproduce an issue, eg. sending an `editor.open` before a `codecomplete.open` so that we have registered the - /// compiler arguments in the SourceKit plugin. - /// - /// These request templates receive the same substitutions as `requestTemplate`. - var contextualRequestTemplates: [String] - /// The offset at which the sourcekitd request should be run. Replaces the /// `$OFFSET` placeholder in the request template. var offset: Int @@ -40,7 +32,7 @@ package struct RequestInfo: Sendable { /// The contents of the file that the sourcekitd request operates on. package var fileContents: String - package func requests(for file: URL) throws -> [String] { + package func request(for file: URL) throws -> String { let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes] guard var compilerArgs = String(data: try encoder.encode(compilerArgs), encoding: .utf8) else { @@ -48,15 +40,11 @@ package struct RequestInfo: Sendable { } // Drop the opening `[` and `]`. The request template already contains them compilerArgs = String(compilerArgs.dropFirst().dropLast()) - let quotedFileContents = - try String(data: JSONEncoder().encode(try String(contentsOf: file, encoding: .utf8)), encoding: .utf8) ?? "" - return try (contextualRequestTemplates + [requestTemplate]).map { requestTemplate in + return requestTemplate - .replacingOccurrences(of: "$OFFSET", with: String(offset)) - .replacingOccurrences(of: "$COMPILER_ARGS", with: compilerArgs) - .replacingOccurrences(of: "$FILE_CONTENTS", with: quotedFileContents) - .replacingOccurrences(of: "$FILE", with: try file.filePath.replacing(#"\"#, with: #"\\"#)) - } + .replacingOccurrences(of: "$OFFSET", with: String(offset)) + .replacingOccurrences(of: "$COMPILER_ARGS", with: compilerArgs) + .replacingOccurrences(of: "$FILE", with: try file.filePath.replacing(#"\"#, with: #"\\"#)) } /// A fake value that is used to indicate that we are reducing a `swift-frontend` issue instead of a sourcekitd issue. @@ -69,15 +57,8 @@ package struct RequestInfo: Sendable { } """ - package init( - requestTemplate: String, - contextualRequestTemplates: [String], - offset: Int, - compilerArgs: [String], - fileContents: String - ) { + package init(requestTemplate: String, offset: Int, compilerArgs: [String], fileContents: String) { self.requestTemplate = requestTemplate - self.contextualRequestTemplates = contextualRequestTemplates self.offset = offset self.compilerArgs = compilerArgs self.fileContents = fileContents @@ -89,17 +70,42 @@ package struct RequestInfo: Sendable { package init(request: String) throws { var requestTemplate = request - requestTemplate.replace(#/ *key.sourcetext: .*\n/#, with: #"key.sourcetext: $FILE_CONTENTS\#n"#) + // Extract offset + let offsetRegex = Regex { + "key.offset: " + Capture(ZeroOrMore(.digit)) + } + if let offsetMatch = requestTemplate.matches(of: offsetRegex).only { + offset = Int(offsetMatch.1)! + requestTemplate.replace(offsetRegex, with: "key.offset: $OFFSET") + } else { + offset = 0 + } - let sourceFilePath: URL - (requestTemplate, offset) = try extractOffset(from: requestTemplate) - (requestTemplate, sourceFilePath) = try extractSourceFile(from: requestTemplate) - (requestTemplate, compilerArgs) = try extractCompilerArguments(from: requestTemplate) + // If the request contained source text, remove it. We want to pick it up from the file on disk and most (possibly + // all) sourcekitd requests use key.sourcefile if key.sourcetext is missing. + requestTemplate.replace(#/ *key.sourcetext: .*\n/#, with: "") + + // Extract source file + let sourceFileRegex = Regex { + #"key.sourcefile: ""# + Capture(ZeroOrMore(#/[^"]/#)) + "\"" + } + guard let sourceFileMatch = requestTemplate.matches(of: sourceFileRegex).only else { + throw GenericError("Failed to find key.sourcefile in the request") + } + let sourceFilePath = String(sourceFileMatch.1) + requestTemplate.replace(sourceFileMatch.1, with: "$FILE") + + // Extract compiler arguments + let compilerArgsExtraction = try extractCompilerArguments(from: requestTemplate) + requestTemplate = compilerArgsExtraction.template + compilerArgs = compilerArgsExtraction.compilerArgs self.requestTemplate = requestTemplate - self.contextualRequestTemplates = [] - fileContents = try String(contentsOf: sourceFilePath, encoding: .utf8) + fileContents = try String(contentsOf: URL(fileURLWithPath: sourceFilePath), encoding: .utf8) } /// Create a `RequestInfo` that is used to reduce a `swift-frontend issue` @@ -133,7 +139,6 @@ package struct RequestInfo: Sendable { // `mergeSwiftFiles`. self.init( requestTemplate: Self.fakeRequestTemplateForFrontendIssues, - contextualRequestTemplates: [], offset: 0, compilerArgs: frontendArgsWithFilelistInlined, fileContents: "" @@ -141,63 +146,6 @@ package struct RequestInfo: Sendable { } } -private func extractOffset(from requestTemplate: String) throws -> (template: String, offset: Int) { - let offsetRegex = Regex { - "key.offset: " - Capture(ZeroOrMore(.digit)) - } - guard let offsetMatch = requestTemplate.matches(of: offsetRegex).only else { - return (requestTemplate, 0) - } - let requestTemplate = requestTemplate.replacing(offsetRegex, with: "key.offset: $OFFSET") - return (requestTemplate, Int(offsetMatch.1)!) -} - -private func extractSourceFile(from requestTemplate: String) throws -> (template: String, sourceFile: URL) { - var requestTemplate = requestTemplate - let sourceFileRegex = Regex { - #"key.sourcefile: ""# - Capture(ZeroOrMore(#/[^"]/#)) - "\"" - } - let nameRegex = Regex { - #"key.name: ""# - Capture(ZeroOrMore(#/[^"]/#)) - "\"" - } - let sourceFileMatch = requestTemplate.matches(of: sourceFileRegex).only - let nameMatch = requestTemplate.matches(of: nameRegex).only - - let sourceFilePath: String? - if let sourceFileMatch { - sourceFilePath = String(sourceFileMatch.1) - requestTemplate.replace(sourceFileMatch.1, with: "$FILE") - } else { - sourceFilePath = nil - } - - let namePath: String? - if let nameMatch { - namePath = String(nameMatch.1) - requestTemplate.replace(nameMatch.1, with: "$FILE") - } else { - namePath = nil - } - switch (sourceFilePath, namePath) { - case (let sourceFilePath?, let namePath?): - if sourceFilePath != namePath { - throw GenericError("Mismatching key.sourcefile and key.name in the request: \(sourceFilePath) vs. \(namePath)") - } - return (requestTemplate, URL(fileURLWithPath: sourceFilePath)) - case (let sourceFilePath?, nil): - return (requestTemplate, URL(fileURLWithPath: sourceFilePath)) - case (nil, let namePath?): - return (requestTemplate, URL(fileURLWithPath: namePath)) - case (nil, nil): - throw GenericError("Failed to find key.sourcefile or key.name in the request") - } -} - private func extractCompilerArguments( from requestTemplate: String ) throws -> (template: String, compilerArgs: [String]) { @@ -212,6 +160,9 @@ private func extractCompilerArguments( } let template = lines[...compilerArgsStartIndex] + ["$COMPILER_ARGS"] + lines[compilerArgsEndIndex...] let compilerArgsJson = "[" + lines[(compilerArgsStartIndex + 1).. SourceKitDRequestResult { - var arguments = [ - ProcessInfo.processInfo.arguments[0], - "debug", - "run-sourcekitd-request", - "--sourcekitd", - try sourcekitd.filePath, - ] - if let pluginPaths { - arguments += [ - "--sourcekit-plugin-path", - try pluginPaths.servicePlugin.filePath, - "--sourcekit-client-plugin-path", - try pluginPaths.clientPlugin.filePath, - ] - } - try request.fileContents.write(to: temporarySourceFile, atomically: true, encoding: .utf8) - let requestStrings = try request.requests(for: temporarySourceFile) - for (index, requestString) in requestStrings.enumerated() { - let temporaryRequestFile = temporaryDirectory.appendingPathComponent("request-\(index).yml") - try requestString.write( - to: temporaryRequestFile, - atomically: true, - encoding: .utf8 - ) - arguments += [ + let requestString = try request.request(for: temporarySourceFile) + try requestString.write(to: temporaryRequestFile, atomically: true, encoding: .utf8) + + let process = Process( + arguments: [ + ProcessInfo.processInfo.arguments[0], + "debug", + "run-sourcekitd-request", + "--sourcekitd", + try sourcekitd.filePath, "--request-file", try temporaryRequestFile.filePath, ] - } + ) + try process.launch() + let result = try await process.waitUntilExit() - let result = try await Process.run(arguments: arguments, workingDirectory: nil) return requestResult(for: result) } } diff --git a/Sources/Diagnose/SourceReducer.swift b/Sources/Diagnose/SourceReducer.swift index 017784dbc..aed94b59e 100644 --- a/Sources/Diagnose/SourceReducer.swift +++ b/Sources/Diagnose/SourceReducer.swift @@ -269,7 +269,6 @@ fileprivate class SourceReducer { let reducedRequestInfo = RequestInfo( requestTemplate: requestInfo.requestTemplate, - contextualRequestTemplates: requestInfo.contextualRequestTemplates, offset: adjustedOffset, compilerArgs: requestInfo.compilerArgs, fileContents: reducedSource @@ -633,7 +632,6 @@ fileprivate func getSwiftInterface( """ let requestInfo = RequestInfo( requestTemplate: requestTemplate, - contextualRequestTemplates: [], offset: 0, compilerArgs: compilerArgs, fileContents: "" diff --git a/Sources/Diagnose/Toolchain+PluginPaths.swift b/Sources/Diagnose/Toolchain+PluginPaths.swift deleted file mode 100644 index bcf673f17..000000000 --- a/Sources/Diagnose/Toolchain+PluginPaths.swift +++ /dev/null @@ -1,23 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2025 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 SourceKitD -import ToolchainRegistry - -extension Toolchain { - var pluginPaths: PluginPaths? { - guard let sourceKitClientPlugin, let sourceKitServicePlugin else { - return nil - } - return PluginPaths(clientPlugin: sourceKitClientPlugin, servicePlugin: sourceKitServicePlugin) - } -} diff --git a/Sources/SKTestSupport/SkipUnless.swift b/Sources/SKTestSupport/SkipUnless.swift index ede127af3..1c8830e23 100644 --- a/Sources/SKTestSupport/SkipUnless.swift +++ b/Sources/SKTestSupport/SkipUnless.swift @@ -259,11 +259,11 @@ package actor SkipUnless { ) do { let response = try await sourcekitd.send( - \.codeCompleteSetPopularAPI, sourcekitd.dictionary([ + sourcekitd.keys.request: sourcekitd.requests.codeCompleteSetPopularAPI, sourcekitd.keys.codeCompleteOptions: [ sourcekitd.keys.useNewAPI: 1 - ] + ], ]), timeout: defaultTimeoutDuration ) diff --git a/Sources/SKTestSupport/SourceKitD+send.swift b/Sources/SKTestSupport/SourceKitD+send.swift index 7879b36bb..c27287ab9 100644 --- a/Sources/SKTestSupport/SourceKitD+send.swift +++ b/Sources/SKTestSupport/SourceKitD+send.swift @@ -10,23 +10,19 @@ // //===----------------------------------------------------------------------===// -package import Csourcekitd package import SourceKitD extension SourceKitD { /// Convenience overload of the `send` function for testing that doesn't restart sourcekitd if it does not respond /// and doesn't pass any file contents. package func send( - _ requestUid: KeyPath, _ request: SKDRequestDictionary, - timeout: Duration = defaultTimeoutDuration + timeout: Duration ) async throws -> SKDResponseDictionary { return try await self.send( - requestUid, request, timeout: timeout, restartTimeout: .seconds(60 * 60 * 24), - documentUrl: nil, fileContents: nil ) } diff --git a/Sources/SKUtilities/CMakeLists.txt b/Sources/SKUtilities/CMakeLists.txt index 8c32531cb..70eec825b 100644 --- a/Sources/SKUtilities/CMakeLists.txt +++ b/Sources/SKUtilities/CMakeLists.txt @@ -24,4 +24,4 @@ target_compile_options(SKUtilitiesForPlugin PRIVATE target_link_libraries(SKUtilitiesForPlugin PRIVATE SKLoggingForPlugin SwiftExtensionsForPlugin - $<$>:Foundation>) + $<$>:Foundation>) \ No newline at end of file diff --git a/Sources/SourceKitD/SourceKitD.swift b/Sources/SourceKitD/SourceKitD.swift index ac1bfaff0..48fd94cab 100644 --- a/Sources/SourceKitD/SourceKitD.swift +++ b/Sources/SourceKitD/SourceKitD.swift @@ -315,45 +315,6 @@ package actor SourceKitD { } } - private struct ContextualRequest { - enum Kind { - case editorOpen - case codeCompleteOpen - } - let kind: Kind - let request: SKDRequestDictionary - } - - private var contextualRequests: [URL: [ContextualRequest]] = [:] - - private func recordContextualRequest( - requestUid: sourcekitd_api_uid_t, - request: SKDRequestDictionary, - documentUrl: URL? - ) { - guard let documentUrl else { - return - } - switch requestUid { - case requests.editorOpen: - contextualRequests[documentUrl] = [ContextualRequest(kind: .editorOpen, request: request)] - case requests.editorClose: - contextualRequests[documentUrl] = nil - case requests.codeCompleteOpen: - contextualRequests[documentUrl, default: []].removeAll(where: { $0.kind == .codeCompleteOpen }) - contextualRequests[documentUrl, default: []].append(ContextualRequest(kind: .codeCompleteOpen, request: request)) - case requests.codeCompleteClose: - contextualRequests[documentUrl, default: []].removeAll(where: { $0.kind == .codeCompleteOpen }) - if contextualRequests[documentUrl]?.isEmpty ?? false { - // This should never happen because we should still have an active `.editorOpen` contextual request but just be - // safe in case we don't. - contextualRequests[documentUrl] = nil - } - default: - break - } - } - /// - Parameters: /// - request: The request to send to sourcekitd. /// - timeout: The maximum duration how long to wait for a response. If no response is returned within this time, @@ -361,16 +322,11 @@ package actor SourceKitD { /// - fileContents: The contents of the file that the request operates on. If sourcekitd crashes, the file contents /// will be logged. package func send( - _ requestUid: KeyPath, _ request: SKDRequestDictionary, timeout: Duration, restartTimeout: Duration, - documentUrl: URL?, fileContents: String? ) async throws -> SKDResponseDictionary { - request.set(keys.request, to: requests[keyPath: requestUid]) - recordContextualRequest(requestUid: requests[keyPath: requestUid], request: request, documentUrl: documentUrl) - let sourcekitdResponse = try await withTimeout(timeout) { let restartTimeoutHandle = TimeoutHandle() do { @@ -431,24 +387,13 @@ package actor SourceKitD { guard let dict = sourcekitdResponse.value else { if sourcekitdResponse.error == .connectionInterrupted { - var log = """ + let log = """ Request: \(request.description) File contents: \(fileContents ?? "") """ - - if let documentUrl { - let contextualRequests = (contextualRequests[documentUrl] ?? []).filter { $0.request !== request } - for (index, contextualRequest) in contextualRequests.enumerated() { - log += """ - - Contextual request \(index + 1) / \(contextualRequests.count): - \(contextualRequest.request.description) - """ - } - } let chunks = splitLongMultilineMessage(message: log) for (index, chunk) in chunks.enumerated() { logger.fault( @@ -469,14 +414,10 @@ package actor SourceKitD { } package func crash() async { - _ = try? await send( - \.crashWithExit, - dictionary([:]), - timeout: .seconds(60), - restartTimeout: .seconds(24 * 60 * 60), - documentUrl: nil, - fileContents: nil - ) + let req = dictionary([ + keys.request: requests.crashWithExit + ]) + _ = try? await send(req, timeout: .seconds(60), restartTimeout: .seconds(24 * 60 * 60), fileContents: nil) } } diff --git a/Sources/SourceKitLSP/Rename.swift b/Sources/SourceKitLSP/Rename.swift index f566b277a..b08fe91b6 100644 --- a/Sources/SourceKitLSP/Rename.swift +++ b/Sources/SourceKitLSP/Rename.swift @@ -353,6 +353,7 @@ extension SwiftLanguageService { } let req = sourcekitd.dictionary([ + keys.request: sourcekitd.requests.nameTranslation, keys.sourceFile: snapshot.uri.pseudoPath, keys.compilerArgs: await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: false)?.compilerArgs as [SKDRequestValue]?, @@ -362,7 +363,7 @@ extension SwiftLanguageService { keys.argNames: sourcekitd.array(name.parameters.map { $0.stringOrWildcard }), ]) - let response = try await send(sourcekitdRequest: \.nameTranslation, req, snapshot: snapshot) + let response = try await sendSourcekitdRequest(req, fileContents: snapshot.text) guard let isZeroArgSelector: Int = response[keys.isZeroArgSelector], let selectorPieces: SKDResponseArray = response[keys.selectorPieces] @@ -404,6 +405,7 @@ extension SwiftLanguageService { name: String ) async throws -> String { let req = sourcekitd.dictionary([ + keys.request: sourcekitd.requests.nameTranslation, keys.sourceFile: snapshot.uri.pseudoPath, keys.compilerArgs: await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: false)?.compilerArgs as [SKDRequestValue]?, @@ -419,7 +421,7 @@ extension SwiftLanguageService { req.set(keys.baseName, to: name) } - let response = try await send(sourcekitdRequest: \.nameTranslation, req, snapshot: snapshot) + let response = try await sendSourcekitdRequest(req, fileContents: snapshot.text) guard let baseName: String = response[keys.baseName] else { throw NameTranslationError.malformedClangToSwiftTranslateNameResponse(response) @@ -884,6 +886,7 @@ extension SwiftLanguageService { ) let skreq = sourcekitd.dictionary([ + keys.request: requests.findRenameRanges, keys.sourceFile: snapshot.uri.pseudoPath, // find-syntactic-rename-ranges is a syntactic sourcekitd request that doesn't use the in-memory file snapshot. // We need to send the source text again. @@ -891,7 +894,7 @@ extension SwiftLanguageService { keys.renameLocations: locations, ]) - let syntacticRenameRangesResponse = try await send(sourcekitdRequest: \.findRenameRanges, skreq, snapshot: snapshot) + let syntacticRenameRangesResponse = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text) guard let categorizedRanges: SKDResponseArray = syntacticRenameRangesResponse[keys.categorizedRanges] else { throw ResponseError.internalError("sourcekitd did not return categorized ranges") } diff --git a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift index 1d77fc41d..299785445 100644 --- a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift +++ b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift @@ -264,6 +264,7 @@ class CodeCompletionSession { self.clientSupportsDocumentationResolve = clientCapabilities.textDocument?.completion?.completionItem?.resolveSupport?.properties.contains("documentation") ?? false + } private func open( @@ -278,6 +279,7 @@ class CodeCompletionSession { let sourcekitdPosition = snapshot.sourcekitdPosition(of: self.position) let req = sourcekitd.dictionary([ + keys.request: sourcekitd.requests.codeCompleteOpen, keys.line: sourcekitdPosition.line, keys.column: sourcekitdPosition.utf8Column, keys.name: uri.pseudoPath, @@ -286,7 +288,7 @@ class CodeCompletionSession { keys.codeCompleteOptions: optionsDictionary(filterText: filterText), ]) - let dict = try await send(sourceKitDRequest: \.codeCompleteOpen, req, snapshot: snapshot) + let dict = try await sendSourceKitdRequest(req, snapshot: snapshot) self.state = .open guard let completions: SKDResponseArray = dict[keys.results] else { @@ -312,6 +314,7 @@ class CodeCompletionSession { logger.info("Updating code completion session: \(self.description) filter=\(filterText)") let sourcekitdPosition = snapshot.sourcekitdPosition(of: self.position) let req = sourcekitd.dictionary([ + keys.request: sourcekitd.requests.codeCompleteUpdate, keys.line: sourcekitdPosition.line, keys.column: sourcekitdPosition.utf8Column, keys.name: uri.pseudoPath, @@ -319,7 +322,7 @@ class CodeCompletionSession { keys.codeCompleteOptions: optionsDictionary(filterText: filterText), ]) - let dict = try await send(sourceKitDRequest: \.codeCompleteUpdate, req, snapshot: snapshot) + let dict = try await sendSourceKitdRequest(req, snapshot: snapshot) guard let completions: SKDResponseArray = dict[keys.results] else { return CompletionList(isIncomplete: false, items: []) } @@ -360,6 +363,7 @@ class CodeCompletionSession { case .open: let sourcekitdPosition = snapshot.sourcekitdPosition(of: self.position) let req = sourcekitd.dictionary([ + keys.request: sourcekitd.requests.codeCompleteClose, keys.line: sourcekitdPosition.line, keys.column: sourcekitdPosition.utf8Column, keys.sourceFile: snapshot.uri.pseudoPath, @@ -367,24 +371,21 @@ class CodeCompletionSession { keys.codeCompleteOptions: [keys.useNewAPI: 1], ]) logger.info("Closing code completion session: \(self.description)") - _ = try? await send(sourceKitDRequest: \.codeCompleteClose, req, snapshot: nil) + _ = try? await sendSourceKitdRequest(req, snapshot: nil) self.state = .closed } } // MARK: - Helpers - private func send( - sourceKitDRequest requestUid: KeyPath & Sendable, + private func sendSourceKitdRequest( _ request: SKDRequestDictionary, snapshot: DocumentSnapshot? ) async throws -> SKDResponseDictionary { try await sourcekitd.send( - requestUid, request, timeout: options.sourcekitdRequestTimeoutOrDefault, restartTimeout: options.semanticServiceRestartTimeoutOrDefault, - documentUrl: snapshot?.uri.arbitrarySchemeURL, fileContents: snapshot?.text ) } @@ -559,17 +560,11 @@ class CodeCompletionSession { var item = item if let itemId = CompletionItemData(fromLSPAny: item.data)?.itemId { let req = sourcekitd.dictionary([ - sourcekitd.keys.identifier: itemId + sourcekitd.keys.request: sourcekitd.requests.codeCompleteDocumentation, + sourcekitd.keys.identifier: itemId, ]) let documentationResponse = await orLog("Retrieving documentation for completion item") { - try await sourcekitd.send( - \.codeCompleteDocumentation, - req, - timeout: timeout, - restartTimeout: restartTimeout, - documentUrl: nil, - fileContents: nil - ) + try await sourcekitd.send(req, timeout: timeout, restartTimeout: restartTimeout, fileContents: nil) } if let docString: String = documentationResponse?[sourcekitd.keys.docBrief] { item.documentation = .markupContent(MarkupContent(kind: .markdown, value: docString)) diff --git a/Sources/SourceKitLSP/Swift/CursorInfo.swift b/Sources/SourceKitLSP/Swift/CursorInfo.swift index e82ccf295..67d954713 100644 --- a/Sources/SourceKitLSP/Swift/CursorInfo.swift +++ b/Sources/SourceKitLSP/Swift/CursorInfo.swift @@ -158,6 +158,7 @@ extension SwiftLanguageService { let keys = self.keys let skreq = sourcekitd.dictionary([ + keys.request: requests.cursorInfo, keys.cancelOnSubsequentRequest: 0, keys.offset: offsetRange.lowerBound, keys.length: offsetRange.upperBound != offsetRange.lowerBound ? offsetRange.count : nil, @@ -169,7 +170,7 @@ extension SwiftLanguageService { appendAdditionalParameters?(skreq) - let dict = try await send(sourcekitdRequest: \.cursorInfo, skreq, snapshot: snapshot) + let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text) var cursorInfoResults: [CursorInfo] = [] if let cursorInfo = CursorInfo(dict, snapshot: snapshot, documentManager: documentManager, sourcekitd: sourcekitd) { diff --git a/Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift b/Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift index 8258010d0..a8aa78159 100644 --- a/Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift +++ b/Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift @@ -107,6 +107,7 @@ actor DiagnosticReportManager { let keys = self.keys let skreq = sourcekitd.dictionary([ + keys.request: requests.diagnostics, keys.sourceFile: snapshot.uri.sourcekitdSourceFile, keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath, keys.compilerArgs: compilerArgs as [SKDRequestValue], @@ -115,11 +116,9 @@ actor DiagnosticReportManager { let dict: SKDResponseDictionary do { dict = try await self.sourcekitd.send( - \.diagnostics, skreq, timeout: options.sourcekitdRequestTimeoutOrDefault, restartTimeout: options.semanticServiceRestartTimeoutOrDefault, - documentUrl: snapshot.uri.arbitrarySchemeURL, fileContents: snapshot.text ) } catch SKDError.requestFailed(let sourcekitdError) { diff --git a/Sources/SourceKitLSP/Swift/GeneratedInterfaceManager.swift b/Sources/SourceKitLSP/Swift/GeneratedInterfaceManager.swift index 3164a758a..6cc7bd015 100644 --- a/Sources/SourceKitLSP/Swift/GeneratedInterfaceManager.swift +++ b/Sources/SourceKitLSP/Swift/GeneratedInterfaceManager.swift @@ -67,13 +67,13 @@ actor GeneratedInterfaceManager { let sourcekitd = swiftLanguageService.sourcekitd for documentToClose in documentsToClose { await orLog("Closing generated interface") { - _ = try await swiftLanguageService.send( - sourcekitdRequest: \.editorClose, + _ = try await swiftLanguageService.sendSourcekitdRequest( sourcekitd.dictionary([ + sourcekitd.keys.request: sourcekitd.requests.editorClose, sourcekitd.keys.name: documentToClose, sourcekitd.keys.cancelBuilds: 0, ]), - snapshot: nil + fileContents: nil ) } } @@ -114,6 +114,7 @@ actor GeneratedInterfaceManager { let keys = sourcekitd.keys let skreq = sourcekitd.dictionary([ + keys.request: sourcekitd.requests.editorOpenInterface, keys.moduleName: document.moduleName, keys.groupName: document.groupName, keys.name: document.sourcekitdDocumentName, @@ -122,7 +123,7 @@ actor GeneratedInterfaceManager { .compilerArgs as [SKDRequestValue]?, ]) - let dict = try await swiftLanguageService.send(sourcekitdRequest: \.editorOpenInterface, skreq, snapshot: nil) + let dict = try await swiftLanguageService.sendSourcekitdRequest(skreq, fileContents: nil) guard let contents: String = dict[keys.sourceText] else { throw ResponseError.unknown("sourcekitd response is missing sourceText") @@ -132,13 +133,13 @@ actor GeneratedInterfaceManager { // Another request raced us to create the generated interface. Discard what we computed here and return the cached // value. await orLog("Closing generated interface created during race") { - _ = try await swiftLanguageService.send( - sourcekitdRequest: \.editorClose, + _ = try await swiftLanguageService.sendSourcekitdRequest( sourcekitd.dictionary([ + keys.request: sourcekitd.requests.editorClose, keys.name: document.sourcekitdDocumentName, keys.cancelBuilds: 0, ]), - snapshot: nil + fileContents: nil ) } return cached @@ -190,15 +191,12 @@ actor GeneratedInterfaceManager { let sourcekitd = swiftLanguageService.sourcekitd let keys = sourcekitd.keys let skreq = sourcekitd.dictionary([ + keys.request: sourcekitd.requests.editorFindUSR, keys.sourceFile: document.sourcekitdDocumentName, keys.usr: usr, ]) - let dict = try await swiftLanguageService.send( - sourcekitdRequest: \.editorFindUSR, - skreq, - snapshot: details.snapshot - ) + let dict = try await swiftLanguageService.sendSourcekitdRequest(skreq, fileContents: details.snapshot.text) guard let offset: Int = dict[keys.offset] else { throw ResponseError.unknown("Missing key 'offset'") } diff --git a/Sources/SourceKitLSP/Swift/MacroExpansion.swift b/Sources/SourceKitLSP/Swift/MacroExpansion.swift index 74749100f..891cf8f92 100644 --- a/Sources/SourceKitLSP/Swift/MacroExpansion.swift +++ b/Sources/SourceKitLSP/Swift/MacroExpansion.swift @@ -95,6 +95,7 @@ actor MacroExpansionManager { let skreq = swiftLanguageService.sourcekitd.dictionary([ keys.cancelOnSubsequentRequest: 0, + keys.request: swiftLanguageService.requests.semanticRefactoring, // Preferred name for e.g. an extracted variable. // Empty string means sourcekitd chooses a name automatically. keys.name: "", @@ -108,7 +109,10 @@ actor MacroExpansionManager { keys.compilerArgs: buildSettings?.compilerArgs as [SKDRequestValue]?, ]) - let dict = try await swiftLanguageService.send(sourcekitdRequest: \.semanticRefactoring, skreq, snapshot: snapshot) + let dict = try await swiftLanguageService.sendSourcekitdRequest( + skreq, + fileContents: snapshot.text + ) guard let expansions = [RefactoringEdit](dict, snapshot, keys) else { throw SemanticRefactoringError.noEditsNeeded(snapshot.uri) } diff --git a/Sources/SourceKitLSP/Swift/RefactoringResponse.swift b/Sources/SourceKitLSP/Swift/RefactoringResponse.swift index be497a59d..fcd5cce08 100644 --- a/Sources/SourceKitLSP/Swift/RefactoringResponse.swift +++ b/Sources/SourceKitLSP/Swift/RefactoringResponse.swift @@ -118,6 +118,7 @@ extension SwiftLanguageService { let skreq = sourcekitd.dictionary([ keys.cancelOnSubsequentRequest: 0, + keys.request: self.requests.semanticRefactoring, // Preferred name for e.g. an extracted variable. // Empty string means sourcekitd chooses a name automatically. keys.name: "", @@ -131,7 +132,7 @@ extension SwiftLanguageService { as [SKDRequestValue]?, ]) - let dict = try await send(sourcekitdRequest: \.semanticRefactoring, skreq, snapshot: snapshot) + let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text) guard let refactor = SemanticRefactoring(refactorCommand.title, dict, snapshot, self.keys) else { throw SemanticRefactoringError.noEditsNeeded(uri) } diff --git a/Sources/SourceKitLSP/Swift/RelatedIdentifiers.swift b/Sources/SourceKitLSP/Swift/RelatedIdentifiers.swift index 229d28881..71662df52 100644 --- a/Sources/SourceKitLSP/Swift/RelatedIdentifiers.swift +++ b/Sources/SourceKitLSP/Swift/RelatedIdentifiers.swift @@ -67,6 +67,7 @@ extension SwiftLanguageService { includeNonEditableBaseNames: Bool ) async throws -> RelatedIdentifiersResponse { let skreq = sourcekitd.dictionary([ + keys.request: requests.relatedIdents, keys.cancelOnSubsequentRequest: 0, keys.offset: snapshot.utf8Offset(of: position), keys.sourceFile: snapshot.uri.sourcekitdSourceFile, @@ -76,7 +77,7 @@ extension SwiftLanguageService { as [SKDRequestValue]?, ]) - let dict = try await send(sourcekitdRequest: \.relatedIdents, skreq, snapshot: snapshot) + let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text) guard let results: SKDResponseArray = dict[self.keys.results] else { throw ResponseError.internalError("sourcekitd response did not contain results") diff --git a/Sources/SourceKitLSP/Swift/SemanticTokens.swift b/Sources/SourceKitLSP/Swift/SemanticTokens.swift index a23255aaa..4679e5828 100644 --- a/Sources/SourceKitLSP/Swift/SemanticTokens.swift +++ b/Sources/SourceKitLSP/Swift/SemanticTokens.swift @@ -27,12 +27,13 @@ extension SwiftLanguageService { } let skreq = sourcekitd.dictionary([ + keys.request: requests.semanticTokens, keys.sourceFile: snapshot.uri.sourcekitdSourceFile, keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath, keys.compilerArgs: compileCommand.compilerArgs as [SKDRequestValue], ]) - let dict = try await send(sourcekitdRequest: \.semanticTokens, skreq, snapshot: snapshot) + let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text) guard let skTokens: SKDResponseArray = dict[keys.semanticTokens] else { return nil diff --git a/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift b/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift index 54b2d7a97..06685db50 100644 --- a/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift +++ b/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift @@ -314,18 +314,15 @@ package actor SwiftLanguageService: LanguageService, Sendable { } } - func send( - sourcekitdRequest requestUid: KeyPath & Sendable, + func sendSourcekitdRequest( _ request: SKDRequestDictionary, - snapshot: DocumentSnapshot? + fileContents: String? ) async throws -> SKDResponseDictionary { try await sourcekitd.send( - requestUid, request, timeout: options.sourcekitdRequestTimeoutOrDefault, restartTimeout: options.semanticServiceRestartTimeoutOrDefault, - documentUrl: snapshot?.uri.arbitrarySchemeURL, - fileContents: snapshot?.text + fileContents: fileContents ) } @@ -442,7 +439,10 @@ extension SwiftLanguageService { /// Tell sourcekitd to crash itself. For testing purposes only. package func crash() async { - _ = try? await send(sourcekitdRequest: \.crashWithExit, sourcekitd.dictionary([:]), snapshot: nil) + let req = sourcekitd.dictionary([ + keys.request: sourcekitd.requests.crashWithExit + ]) + _ = try? await sendSourcekitdRequest(req, fileContents: nil) } // MARK: - Build System Integration @@ -465,7 +465,7 @@ extension SwiftLanguageService { let closeReq = closeDocumentSourcekitdRequest(uri: snapshot.uri) _ = await orLog("Closing document to re-open it") { - try await self.send(sourcekitdRequest: \.editorClose, closeReq, snapshot: nil) + try await self.sendSourcekitdRequest(closeReq, fileContents: nil) } let buildSettings = await compileCommand(for: snapshot.uri, fallbackAfterTimeout: true) @@ -475,7 +475,7 @@ extension SwiftLanguageService { ) self.buildSettingsForOpenFiles[snapshot.uri] = buildSettings _ = await orLog("Re-opening document") { - try await self.send(sourcekitdRequest: \.editorOpen, openReq, snapshot: snapshot) + try await self.sendSourcekitdRequest(openReq, fileContents: snapshot.text) } if await capabilityRegistry.clientSupportsPullDiagnostics(for: .swift) { @@ -507,7 +507,10 @@ extension SwiftLanguageService { } await orLog("Sending dependencyUpdated request to sourcekitd") { - _ = try await self.send(sourcekitdRequest: \.dependencyUpdated, sourcekitd.dictionary([:]), snapshot: nil) + let req = sourcekitd.dictionary([ + keys.request: requests.dependencyUpdated + ]) + _ = try await self.sendSourcekitdRequest(req, fileContents: nil) } // Even after sending the `dependencyUpdated` request to sourcekitd, the code completion session has state from // before the AST update. Close it and open a new code completion session on the next completion request. @@ -526,6 +529,7 @@ extension SwiftLanguageService { compileCommand: SwiftCompileCommand? ) -> SKDRequestDictionary { return sourcekitd.dictionary([ + keys.request: self.requests.editorOpen, keys.name: snapshot.uri.pseudoPath, keys.sourceText: snapshot.text, keys.enableSyntaxMap: 0, @@ -538,6 +542,7 @@ extension SwiftLanguageService { func closeDocumentSourcekitdRequest(uri: DocumentURI) -> SKDRequestDictionary { return sourcekitd.dictionary([ + keys.request: requests.editorClose, keys.name: uri.pseudoPath, keys.cancelBuilds: 0, ]) @@ -560,7 +565,7 @@ extension SwiftLanguageService { let req = openDocumentSourcekitdRequest(snapshot: snapshot, compileCommand: buildSettings) await orLog("Opening sourcekitd document") { - _ = try await self.send(sourcekitdRequest: \.editorOpen, req, snapshot: snapshot) + _ = try await self.sendSourcekitdRequest(req, fileContents: snapshot.text) } await publishDiagnosticsIfNeeded(for: notification.textDocument.uri) } @@ -579,7 +584,7 @@ extension SwiftLanguageService { case nil: let req = closeDocumentSourcekitdRequest(uri: notification.textDocument.uri) await orLog("Closing sourcekitd document") { - _ = try await self.send(sourcekitdRequest: \.editorClose, req, snapshot: nil) + _ = try await self.sendSourcekitdRequest(req, fileContents: nil) } } } @@ -677,6 +682,7 @@ extension SwiftLanguageService { for edit in edits { let req = sourcekitd.dictionary([ + keys.request: self.requests.editorReplaceText, keys.name: notification.textDocument.uri.pseudoPath, keys.enableSyntaxMap: 0, keys.enableStructure: 0, @@ -687,7 +693,7 @@ extension SwiftLanguageService { keys.sourceText: edit.replacement, ]) do { - _ = try await self.send(sourcekitdRequest: \.editorReplaceText, req, snapshot: nil) + _ = try await self.sendSourcekitdRequest(req, fileContents: nil) } catch { logger.fault( """ diff --git a/Sources/SourceKitLSP/Swift/VariableTypeInfo.swift b/Sources/SourceKitLSP/Swift/VariableTypeInfo.swift index 9c0346911..e1984113d 100644 --- a/Sources/SourceKitLSP/Swift/VariableTypeInfo.swift +++ b/Sources/SourceKitLSP/Swift/VariableTypeInfo.swift @@ -87,6 +87,7 @@ extension SwiftLanguageService { let skreq = sourcekitd.dictionary([ keys.cancelOnSubsequentRequest: 0, + keys.request: requests.collectVariableType, keys.sourceFile: snapshot.uri.sourcekitdSourceFile, keys.primaryFile: snapshot.uri.primaryFile?.pseudoPath, keys.compilerArgs: await self.compileCommand(for: uri, fallbackAfterTimeout: false)?.compilerArgs @@ -100,7 +101,7 @@ extension SwiftLanguageService { skreq.set(keys.length, to: end - start) } - let dict = try await send(sourcekitdRequest: \.collectVariableType, skreq, snapshot: snapshot) + let dict = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text) guard let skVariableTypeInfos: SKDResponseArray = dict[keys.variableTypeList] else { return [] } diff --git a/Sources/SourceKitLSP/Swift/WithSnapshotFromDiskOpenedInSourcekitd.swift b/Sources/SourceKitLSP/Swift/WithSnapshotFromDiskOpenedInSourcekitd.swift index 27918784b..6348b8015 100644 --- a/Sources/SourceKitLSP/Swift/WithSnapshotFromDiskOpenedInSourcekitd.swift +++ b/Sources/SourceKitLSP/Swift/WithSnapshotFromDiskOpenedInSourcekitd.swift @@ -46,10 +46,9 @@ extension SwiftLanguageService { nil } - _ = try await send( - sourcekitdRequest: \.editorOpen, + _ = try await sendSourcekitdRequest( self.openDocumentSourcekitdRequest(snapshot: snapshot, compileCommand: patchedCompileCommand), - snapshot: snapshot + fileContents: snapshot.text ) let result: Swift.Result do { @@ -58,10 +57,9 @@ extension SwiftLanguageService { result = .failure(error) } await orLog("Close helper document '\(snapshot.uri)' for cursorInfoFromDisk") { - _ = try await send( - sourcekitdRequest: \.editorClose, + _ = try await sendSourcekitdRequest( self.closeDocumentSourcekitdRequest(uri: snapshot.uri), - snapshot: snapshot + fileContents: snapshot.text ) } return try result.get() diff --git a/Tests/DiagnoseTests/DiagnoseTests.swift b/Tests/DiagnoseTests/DiagnoseTests.swift index 2650faa17..9e7522250 100644 --- a/Tests/DiagnoseTests/DiagnoseTests.swift +++ b/Tests/DiagnoseTests/DiagnoseTests.swift @@ -307,7 +307,6 @@ private class InProcessSourceKitRequestExecutor: SourceKitRequestExecutor { func runSwiftFrontend(request: RequestInfo) async throws -> SourceKitDRequestResult { return try await OutOfProcessSourceKitRequestExecutor( sourcekitd: sourcekitd, - pluginPaths: sourceKitPluginPaths, swiftFrontend: swiftFrontend, reproducerPredicate: reproducerPredicate ).runSwiftFrontend(request: request) @@ -315,21 +314,14 @@ private class InProcessSourceKitRequestExecutor: SourceKitRequestExecutor { func runSourceKitD(request: RequestInfo) async throws -> SourceKitDRequestResult { try request.fileContents.write(to: temporarySourceFile, atomically: true, encoding: .utf8) - let requestStrings = try request.requests(for: temporarySourceFile) - logger.info("Sending request: \(requestStrings.joined(separator: "\n\n\n\n"))") + let requestString = try request.request(for: temporarySourceFile) + logger.info("Sending request: \(requestString)") let sourcekitd = try await SourceKitD.getOrCreate( dylibPath: sourcekitd, pluginPaths: sourceKitPluginPaths ) - var response: SKDResponse? = nil - for requestString in requestStrings { - response = try await sourcekitd.run(requestYaml: requestString) - } - guard let response else { - logger.error("No request executed") - return .error - } + let response = try await sourcekitd.run(requestYaml: requestString) logger.info("Received response: \(response.description)") diff --git a/Tests/SourceKitDTests/SourceKitDTests.swift b/Tests/SourceKitDTests/SourceKitDTests.swift index 137a50101..8a9650cd2 100644 --- a/Tests/SourceKitDTests/SourceKitDTests.swift +++ b/Tests/SourceKitDTests/SourceKitDTests.swift @@ -76,6 +76,7 @@ final class SourceKitDTests: XCTestCase { args.append(path) let req = sourcekitd.dictionary([ + keys.request: sourcekitd.requests.editorOpen, keys.name: path, keys.sourceText: """ func foo() {} @@ -83,14 +84,15 @@ final class SourceKitDTests: XCTestCase { keys.compilerArgs: args, ]) - _ = try await sourcekitd.send(\.editorOpen, req, timeout: defaultTimeoutDuration) + _ = try await sourcekitd.send(req, timeout: defaultTimeoutDuration) try await fulfillmentOfOrThrow(expectation1, expectation2) let close = sourcekitd.dictionary([ - keys.name: path + keys.request: sourcekitd.requests.editorClose, + keys.name: path, ]) - _ = try await sourcekitd.send(\.editorClose, close, timeout: defaultTimeoutDuration) + _ = try await sourcekitd.send(close, timeout: defaultTimeoutDuration) } } diff --git a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift index 59bd49884..b99e69b36 100644 --- a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift +++ b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift @@ -1824,17 +1824,19 @@ fileprivate extension SourceKitD { compilerArguments += ["-sdk", defaultSDKPath] } let req = dictionary([ + keys.request: requests.editorOpen, keys.name: name, keys.sourceText: textWithoutMarkers, keys.syntacticOnly: 1, keys.compilerArgs: compilerArguments as [SKDRequestValue], ]) - _ = try await send(\.editorOpen, req) + _ = try await send(req, timeout: defaultTimeoutDuration) return DocumentPositions(markers: markers, textWithoutMarkers: textWithoutMarkers) } nonisolated func editDocument(_ name: String, fromOffset offset: Int, length: Int, newContents: String) async throws { let req = dictionary([ + keys.request: requests.editorReplaceText, keys.name: name, keys.offset: offset, keys.length: length, @@ -1842,19 +1844,20 @@ fileprivate extension SourceKitD { keys.syntacticOnly: 1, ]) - _ = try await send(\.editorReplaceText, req) + _ = try await send(req, timeout: defaultTimeoutDuration) } nonisolated func closeDocument(_ name: String) async throws { let req = dictionary([ - keys.name: name + keys.request: requests.editorClose, + keys.name: name, ]) - _ = try await send(\.editorClose, req) + _ = try await send(req, timeout: defaultTimeoutDuration) } nonisolated func completeImpl( - requestUID: KeyPath & Sendable, + requestUID: sourcekitd_api_uid_t, path: String, position: Position, filter: String, @@ -1876,6 +1879,7 @@ fileprivate extension SourceKitD { ]) let req = dictionary([ + keys.request: requestUID, keys.line: position.line + 1, // Technically sourcekitd needs a UTF-8 index but we can assume there are no Unicode characters in the tests keys.column: position.utf16index + 1, @@ -1884,7 +1888,7 @@ fileprivate extension SourceKitD { keys.compilerArgs: compilerArguments as [SKDRequestValue]?, ]) - let res = try await send(requestUID, req) + let res = try await send(req, timeout: defaultTimeoutDuration) return try CompletionResultSet(res) } @@ -1899,7 +1903,7 @@ fileprivate extension SourceKitD { compilerArguments: [String]? = nil ) async throws -> CompletionResultSet { return try await completeImpl( - requestUID: \.codeCompleteOpen, + requestUID: requests.codeCompleteOpen, path: path, position: position, filter: filter, @@ -1920,7 +1924,7 @@ fileprivate extension SourceKitD { maxResults: Int? = nil ) async throws -> CompletionResultSet { return try await completeImpl( - requestUID: \.codeCompleteUpdate, + requestUID: requests.codeCompleteUpdate, path: path, position: position, filter: filter, @@ -1934,6 +1938,7 @@ fileprivate extension SourceKitD { nonisolated func completeClose(path: String, position: Position) async throws { let req = dictionary([ + keys.request: requests.codeCompleteClose, keys.line: position.line + 1, // Technically sourcekitd needs a UTF-8 index but we can assume there are no Unicode characters in the tests keys.column: position.utf16index + 1, @@ -1941,32 +1946,45 @@ fileprivate extension SourceKitD { keys.codeCompleteOptions: dictionary([keys.useNewAPI: 1]), ]) - _ = try await send(\.codeCompleteClose, req) + _ = try await send(req, timeout: defaultTimeoutDuration) } nonisolated func completeDocumentation(id: Int) async throws -> CompletionDocumentation { - let resp = try await send(\.codeCompleteDocumentation, dictionary([keys.identifier: id])) + let req = dictionary([ + keys.request: requests.codeCompleteDocumentation, + keys.identifier: id, + ]) + + let resp = try await send(req, timeout: defaultTimeoutDuration) return CompletionDocumentation(resp) } nonisolated func completeDiagnostic(id: Int) async throws -> CompletionDiagnostic? { - let resp = try await send(\.codeCompleteDiagnostic, dictionary([keys.identifier: id])) + let req = dictionary([ + keys.request: requests.codeCompleteDiagnostic, + keys.identifier: id, + ]) + let resp = try await send(req, timeout: defaultTimeoutDuration) return CompletionDiagnostic(resp) } nonisolated func dependencyUpdated() async throws { - _ = try await send(\.dependencyUpdated, dictionary([:])) + let req = dictionary([ + keys.request: requests.dependencyUpdated + ]) + _ = try await send(req, timeout: defaultTimeoutDuration) } nonisolated func setPopularAPI(popular: [String], unpopular: [String]) async throws { let req = dictionary([ + keys.request: requests.codeCompleteSetPopularAPI, keys.codeCompleteOptions: dictionary([keys.useNewAPI: 1]), keys.popular: popular as [SKDRequestValue], keys.unpopular: unpopular as [SKDRequestValue], ]) - let resp = try await send(\.codeCompleteSetPopularAPI, req) + let resp = try await send(req, timeout: defaultTimeoutDuration) XCTAssertEqual(resp[keys.useNewAPI], 1) } @@ -1976,13 +1994,14 @@ fileprivate extension SourceKitD { notoriousModules: [String] ) async throws { let req = dictionary([ + keys.request: requests.codeCompleteSetPopularAPI, keys.codeCompleteOptions: dictionary([keys.useNewAPI: 1]), keys.scopedPopularityTablePath: scopedPopularityDataPath, keys.popularModules: popularModules as [SKDRequestValue], keys.notoriousModules: notoriousModules as [SKDRequestValue], ]) - let resp = try await send(\.codeCompleteSetPopularAPI, req) + let resp = try await send(req, timeout: defaultTimeoutDuration) XCTAssertEqual(resp[keys.useNewAPI], 1) } @@ -2000,12 +2019,13 @@ fileprivate extension SourceKitD { ]) } let req = dictionary([ + keys.request: requests.codeCompleteSetPopularAPI, keys.codeCompleteOptions: dictionary([keys.useNewAPI: 1]), keys.symbolPopularity: symbolPopularity as [SKDRequestValue], keys.modulePopularity: modulePopularity as [SKDRequestValue], ]) - let resp = try await send(\.codeCompleteSetPopularAPI, req) + let resp = try await send(req, timeout: defaultTimeoutDuration) XCTAssertEqual(resp[keys.useNewAPI], 1) }