From 9ca89669b21094f9c4c8eac6ae2e95903f5776b6 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Sat, 6 May 2023 15:45:23 +0100 Subject: [PATCH] Swiftinterface symbol lookup Generate swiftinterface for symbol lookup When a symbol definition returns it is in a swiftinterface file, create textual version of swiftinterface and return that in response. Extend OpenInterface to also seatch for a symbol Fix warning Syntax changes after review Move module name split into OpenInterfaceRequest Use group names when running open interface request Requested changes from PR rename symbol to symbolUSR Cleanup OpenInterfaceRequest.init Fix tests Added testDefinitionInSystemModuleInterface Use SwiftPMPackage test module Added version of buildAndIndex that includes system symbols Merge buildAndIndexWithSystemSymbols with buildAndIndex Added specific test project for system swiftinterface tests Add multiple tests for various system modules --- .../Requests/OpenInterfaceRequest.swift | 29 +++++- .../INPUTS/SystemSwiftInterface/Package.swift | 16 +++ .../Sources/lib/lib.swift | 10 ++ .../SKSwiftPMTestWorkspace.swift | 19 ++-- Sources/SourceKitD/sourcekitd_uids.swift | 4 + Sources/SourceKitLSP/SourceKitServer.swift | 51 +++++++--- .../SourceKitLSP/Swift/OpenInterface.swift | 99 ++++++++++++++++--- .../SwiftInterfaceTests.swift | 59 ++++++++++- 8 files changed, 248 insertions(+), 39 deletions(-) create mode 100644 Sources/SKTestSupport/INPUTS/SystemSwiftInterface/Package.swift create mode 100644 Sources/SKTestSupport/INPUTS/SystemSwiftInterface/Sources/lib/lib.swift diff --git a/Sources/LanguageServerProtocol/Requests/OpenInterfaceRequest.swift b/Sources/LanguageServerProtocol/Requests/OpenInterfaceRequest.swift index 6292c1d9f..aa9a1b2e4 100644 --- a/Sources/LanguageServerProtocol/Requests/OpenInterfaceRequest.swift +++ b/Sources/LanguageServerProtocol/Requests/OpenInterfaceRequest.swift @@ -20,11 +20,30 @@ public struct OpenInterfaceRequest: TextDocumentRequest, Hashable { public var textDocument: TextDocumentIdentifier /// The module to generate an index for. - public var name: String + public var moduleName: String - public init(textDocument: TextDocumentIdentifier, name: String) { + /// The module group name. + public var groupNames: [String] + + /// The symbol USR to search for in the generated module interface. + public var symbolUSR: String? + + public init(textDocument: TextDocumentIdentifier, name: String, symbolUSR: String?) { self.textDocument = textDocument - self.name = name + self.symbolUSR = symbolUSR + // Stdlib Swift modules are all in the "Swift" module, but their symbols return a module name `Swift.***`. + let splitName = name.split(separator: ".") + self.moduleName = String(splitName[0]) + self.groupNames = [String.SubSequence](splitName.dropFirst()).map(String.init) + } + + /// Name of interface module name with group names appended + public var name: String { + if groupNames.count > 0 { + return "\(self.moduleName).\(self.groupNames.joined(separator: "."))" + } else { + return self.moduleName + } } } @@ -32,8 +51,10 @@ public struct OpenInterfaceRequest: TextDocumentRequest, Hashable { public struct InterfaceDetails: ResponseType, Hashable { public var uri: DocumentURI + public var position: Position? - public init(uri: DocumentURI) { + public init(uri: DocumentURI, position: Position?) { self.uri = uri + self.position = position } } diff --git a/Sources/SKTestSupport/INPUTS/SystemSwiftInterface/Package.swift b/Sources/SKTestSupport/INPUTS/SystemSwiftInterface/Package.swift new file mode 100644 index 000000000..c93201271 --- /dev/null +++ b/Sources/SKTestSupport/INPUTS/SystemSwiftInterface/Package.swift @@ -0,0 +1,16 @@ +// swift-tools-version:5.1 + +import PackageDescription + +let package = Package( + name: "SystemSwiftInterface", + platforms: [.macOS(.v10_15)], + products: [], + dependencies: [], + targets: [ + .target( + name: "lib", + dependencies: []), + /*Package.swift:targets*/ + ] +) diff --git a/Sources/SKTestSupport/INPUTS/SystemSwiftInterface/Sources/lib/lib.swift b/Sources/SKTestSupport/INPUTS/SystemSwiftInterface/Sources/lib/lib.swift new file mode 100644 index 000000000..2acbd3c78 --- /dev/null +++ b/Sources/SKTestSupport/INPUTS/SystemSwiftInterface/Sources/lib/lib.swift @@ -0,0 +1,10 @@ +public func libFunc() async { + let a: /*lib.string*/String = "test" + let i: /*lib.integer*/Int = 2 + await /*lib.withTaskGroup*/withTaskGroup(of: Void.self) { group in + group.addTask { + print(a) + print(i) + } + } +} \ No newline at end of file diff --git a/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift b/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift index bcd54c896..f5b97dd00 100644 --- a/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift +++ b/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift @@ -120,20 +120,25 @@ extension SKSwiftPMTestWorkspace { public func testLoc(_ name: String) -> TestLocation { sources.locations[name]! } - public func buildAndIndex() throws { - try build() + public func buildAndIndex(withSystemSymbols: Bool = false) throws { + try build(withSystemSymbols: withSystemSymbols) index.pollForUnitChangesAndWait() } - func build() throws { - try TSCBasic.Process.checkNonZeroExit(arguments: [ + func build(withSystemSymbols: Bool = false) throws { + var arguments = [ toolchain.swift!.pathString, "build", "--package-path", sources.rootDirectory.path, "--scratch-path", buildDir.path, - "-Xswiftc", "-index-ignore-system-modules", - "-Xcc", "-index-ignore-system-symbols", - ]) + ] + if !withSystemSymbols { + arguments.append(contentsOf: [ + "-Xswiftc", "-index-ignore-system-modules", + "-Xcc", "-index-ignore-system-symbols", + ]) + } + try TSCBasic.Process.checkNonZeroExit(arguments: arguments) } } diff --git a/Sources/SourceKitD/sourcekitd_uids.swift b/Sources/SourceKitD/sourcekitd_uids.swift index d34ba191c..b88dd9390 100644 --- a/Sources/SourceKitD/sourcekitd_uids.swift +++ b/Sources/SourceKitD/sourcekitd_uids.swift @@ -40,6 +40,7 @@ public struct sourcekitd_keys { public let expression_type_list: sourcekitd_uid_t public let filepath: sourcekitd_uid_t public let fixits: sourcekitd_uid_t + public let groupname: sourcekitd_uid_t public let id: sourcekitd_uid_t public let is_system: sourcekitd_uid_t public let kind: sourcekitd_uid_t @@ -115,6 +116,7 @@ public struct sourcekitd_keys { expression_type_list = api.uid_get_from_cstr("key.expression_type_list")! filepath = api.uid_get_from_cstr("key.filepath")! fixits = api.uid_get_from_cstr("key.fixits")! + groupname = api.uid_get_from_cstr("key.groupname")! id = api.uid_get_from_cstr("key.id")! is_system = api.uid_get_from_cstr("key.is_system")! kind = api.uid_get_from_cstr("key.kind")! @@ -176,6 +178,7 @@ public struct sourcekitd_requests { public let codecomplete_close: sourcekitd_uid_t public let cursorinfo: sourcekitd_uid_t public let expression_type: sourcekitd_uid_t + public let find_usr: sourcekitd_uid_t public let variable_type: sourcekitd_uid_t public let relatedidents: sourcekitd_uid_t public let semantic_refactoring: sourcekitd_uid_t @@ -192,6 +195,7 @@ public struct sourcekitd_requests { codecomplete_close = api.uid_get_from_cstr("source.request.codecomplete.close")! cursorinfo = api.uid_get_from_cstr("source.request.cursorinfo")! expression_type = api.uid_get_from_cstr("source.request.expression.type")! + find_usr = api.uid_get_from_cstr("source.request.editor.find_usr")! variable_type = api.uid_get_from_cstr("source.request.variable.type")! relatedidents = api.uid_get_from_cstr("source.request.relatedidents")! semantic_refactoring = api.uid_get_from_cstr("source.request.semantic.refactoring")! diff --git a/Sources/SourceKitLSP/SourceKitServer.swift b/Sources/SourceKitLSP/SourceKitServer.swift index 5dbf88fdf..b9f51efde 100644 --- a/Sources/SourceKitLSP/SourceKitServer.swift +++ b/Sources/SourceKitLSP/SourceKitServer.swift @@ -1292,20 +1292,7 @@ extension SourceKitServer { // If this symbol is a module then generate a textual interface if case .success(let symbols) = result, let symbol = symbols.first, symbol.kind == .module, let name = symbol.name { - let openInterface = OpenInterfaceRequest(textDocument: req.params.textDocument, name: name) - let request = Request(openInterface, id: req.id, clientID: ObjectIdentifier(self), - cancellation: req.cancellationToken, reply: { (result: Result) in - switch result { - case .success(let interfaceDetails?): - let loc = Location(uri: interfaceDetails.uri, range: Range(Position(line: 0, utf16index: 0))) - req.reply(.locations([loc])) - case .success(nil): - req.reply(.failure(.unknown("Could not generate Swift Interface for \(name)"))) - case .failure(let error): - req.reply(.failure(error)) - } - }) - languageService.openInterface(request) + self.respondWithInterface(req, moduleName: name, symbolUSR: nil, languageService: languageService) return } @@ -1320,6 +1307,19 @@ extension SourceKitServer { switch extractedResult { case .success(let resolved): + // if first resolved location is in `.swiftinterface` file. Use moduleName to return + // textual interface + if let firstResolved = resolved.first, + let moduleName = firstResolved.occurrence?.location.moduleName, + firstResolved.location.uri.fileURL?.pathExtension == "swiftinterface" { + self.respondWithInterface( + req, + moduleName: moduleName, + symbolUSR: firstResolved.occurrence?.symbol.usr, + languageService: languageService + ) + return + } let locs = resolved.map(\.location) // If we're unable to handle the definition request using our index, see if the // language service can handle it (e.g. clangd can provide AST based definitions). @@ -1339,6 +1339,29 @@ extension SourceKitServer { languageService.symbolInfo(request) } + func respondWithInterface( + _ req: Request, + moduleName: String, + symbolUSR: String?, + languageService: ToolchainLanguageServer + ) { + let openInterface = OpenInterfaceRequest(textDocument: req.params.textDocument, name: moduleName, symbolUSR: symbolUSR) + let request = Request(openInterface, id: req.id, clientID: ObjectIdentifier(self), + cancellation: req.cancellationToken, reply: { (result: Result) in + switch result { + case .success(let interfaceDetails?): + let position = interfaceDetails.position ?? Position(line: 0, utf16index: 0) + let loc = Location(uri: interfaceDetails.uri, range: Range(position)) + req.reply(.locations([loc])) + case .success(nil): + req.reply(.failure(.unknown("Could not generate Swift Interface for \(moduleName)"))) + case .failure(let error): + req.reply(.failure(error)) + } + }) + languageService.openInterface(request) + } + func implementation( _ req: Request, workspace: Workspace, diff --git a/Sources/SourceKitLSP/Swift/OpenInterface.swift b/Sources/SourceKitLSP/Swift/OpenInterface.swift index 6e3be3887..a35391416 100644 --- a/Sources/SourceKitLSP/Swift/OpenInterface.swift +++ b/Sources/SourceKitLSP/Swift/OpenInterface.swift @@ -14,30 +14,46 @@ import Foundation import SourceKitD import LanguageServerProtocol import LSPLogging +import SKSupport struct InterfaceInfo { var contents: String } +struct FindUSRInfo { + let position: Position? +} + extension SwiftLanguageServer { public func openInterface(_ request: LanguageServerProtocol.Request) { let uri = request.params.textDocument.uri - let moduleName = request.params.name + let moduleName = request.params.moduleName + let name = request.params.name + let symbol = request.params.symbolUSR self.queue.async { - let interfaceFilePath = self.generatedInterfacesPath.appendingPathComponent("\(moduleName).swiftinterface") + let interfaceFilePath = self.generatedInterfacesPath.appendingPathComponent("\(name).swiftinterface") let interfaceDocURI = DocumentURI(interfaceFilePath) - self._openInterface(request: request, uri: uri, name: moduleName, interfaceURI: interfaceDocURI) { result in - switch result { - case .success(let interfaceInfo): - do { - try interfaceInfo.contents.write(to: interfaceFilePath, atomically: true, encoding: String.Encoding.utf8) - request.reply(.success(InterfaceDetails(uri: interfaceDocURI))) - } catch { - request.reply(.failure(ResponseError.unknown(error.localizedDescription))) + // has interface already been generated + if let snapshot = self.documentManager.latestSnapshot(interfaceDocURI) { + self._findUSRAndRespond(request: request, uri: interfaceDocURI, snapshot: snapshot, symbol: symbol) + } else { + // generate interface + self._openInterface(request: request, uri: uri, name: moduleName, interfaceURI: interfaceDocURI) { result in + switch result { + case .success(let interfaceInfo): + do { + // write to file + try interfaceInfo.contents.write(to: interfaceFilePath, atomically: true, encoding: String.Encoding.utf8) + // store snapshot + let snapshot = try self.documentManager.open(interfaceDocURI, language: .swift, version: 0, text: interfaceInfo.contents) + self._findUSRAndRespond(request: request, uri: interfaceDocURI, snapshot: snapshot, symbol: symbol) + } catch { + request.reply(.failure(ResponseError.unknown(error.localizedDescription))) + } + case .failure(let error): + log("open interface failed: \(error)", level: .warning) + request.reply(.failure(ResponseError(error))) } - case .failure(let error): - log("open interface failed: \(error)", level: .warning) - request.reply(.failure(ResponseError(error))) } } } @@ -60,6 +76,9 @@ extension SwiftLanguageServer { let skreq = SKDRequestDictionary(sourcekitd: sourcekitd) skreq[keys.request] = requests.editor_open_interface skreq[keys.modulename] = name + if request.params.groupNames.count > 0 { + skreq[keys.groupname] = request.params.groupNames + } skreq[keys.name] = interfaceURI.pseudoPath skreq[keys.synthesizedextensions] = 1 if let compileCommand = self.commandsByFile[uri] { @@ -81,4 +100,58 @@ extension SwiftLanguageServer { } } } + + private func _findUSRAndRespond( + request: LanguageServerProtocol.Request, + uri: DocumentURI, + snapshot: DocumentSnapshot, + symbol: String? + ) { + self._findUSR(request: request, uri: uri, snapshot: snapshot, symbol: symbol) { result in + switch result { + case .success(let info): + request.reply(.success(InterfaceDetails(uri: uri, position: info.position))) + case .failure: + request.reply(.success(InterfaceDetails(uri: uri, position: nil))) + } + } + } + + private func _findUSR( + request: LanguageServerProtocol.Request, + uri: DocumentURI, + snapshot: DocumentSnapshot, + symbol: String?, + completion: @escaping (Swift.Result) -> Void + ) { + guard let symbol = symbol else { + return completion(.success(FindUSRInfo(position: nil))) + } + let keys = self.keys + let skreq = SKDRequestDictionary(sourcekitd: sourcekitd) + + skreq[keys.request] = requests.find_usr + skreq[keys.sourcefile] = uri.pseudoPath + skreq[keys.usr] = symbol + + let handle = self.sourcekitd.send(skreq, self.queue) { result in + switch result { + case .success(let dict): + if let offset: Int = dict[keys.offset], + let position = snapshot.positionOf(utf8Offset: offset) { + return completion(.success(FindUSRInfo(position: position))) + } else { + return completion(.success(FindUSRInfo(position: nil))) + } + case .failure(let error): + return completion(.failure(error)) + } + } + + if let handle = handle { + request.cancellationToken.addCancellationHandler { [weak self] in + self?.sourcekitd.cancel(handle) + } + } + } } diff --git a/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift b/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift index 513756cdc..25850fd79 100644 --- a/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift +++ b/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift @@ -11,9 +11,11 @@ //===----------------------------------------------------------------------===// import Foundation +import ISDBTestSupport import LanguageServerProtocol import LSPTestSupport import LSPLogging +import SKSupport import SKTestSupport import SourceKitLSP import XCTest @@ -99,7 +101,7 @@ final class SwiftInterfaceTests: XCTestCase { try ws.buildAndIndex() let importedModule = ws.testLoc("lib:import") try ws.openDocument(importedModule.url, language: .swift) - let openInterface = OpenInterfaceRequest(textDocument: importedModule.docIdentifier, name: "lib") + let openInterface = OpenInterfaceRequest(textDocument: importedModule.docIdentifier, name: "lib", symbolUSR: nil) let interfaceDetails = try XCTUnwrap(ws.sk.sendSync(openInterface)) XCTAssertTrue(interfaceDetails.uri.pseudoPath.hasSuffix("/lib.swiftinterface")) let fileContents = try XCTUnwrap(interfaceDetails.uri.fileURL.flatMap({ try String(contentsOf: $0, encoding: .utf8) })) @@ -113,6 +115,61 @@ final class SwiftInterfaceTests: XCTestCase { """)) } + /// Used by testDefinitionInSystemModuleInterface + func testSystemSwiftInterface( + _ testLoc: TestLocation, + ws: SKSwiftPMTestWorkspace, + swiftInterfaceFile: String, + linePrefix: String + ) throws { + try ws.openDocument(testLoc.url, language: .swift) + let definition = try ws.sk.sendSync(DefinitionRequest( + textDocument: testLoc.docIdentifier, + position: testLoc.position)) + guard case .locations(let jump) = definition else { + XCTFail("Response is not locations") + return + } + let location = try XCTUnwrap(jump.first) + XCTAssertTrue(location.uri.pseudoPath.hasSuffix(swiftInterfaceFile)) + // load contents of swiftinterface + let contents = try XCTUnwrap(location.uri.fileURL.flatMap({ try String(contentsOf: $0, encoding: .utf8) })) + let lineTable = LineTable(contents) + let line = lineTable[location.range.lowerBound.line] + XCTAssert(line.hasPrefix(linePrefix)) + ws.closeDocument(testLoc.url) + } + + func testDefinitionInSystemModuleInterface() throws { + guard let ws = try staticSourceKitSwiftPMWorkspace(name: "SystemSwiftInterface") else { return } + try ws.buildAndIndex(withSystemSymbols: true) + let stringRef = ws.testLoc("lib.string") + let intRef = ws.testLoc("lib.integer") + let withTaskGroupRef = ws.testLoc("lib.withTaskGroup") + + // Test stdlib with one submodule + try testSystemSwiftInterface( + stringRef, + ws: ws, + swiftInterfaceFile: "/Swift.String.swiftinterface", + linePrefix: "@frozen public struct String" + ) + // Test stdlib with two submodules + try testSystemSwiftInterface( + intRef, + ws: ws, + swiftInterfaceFile: "/Swift.Math.Integers.swiftinterface", + linePrefix: "@frozen public struct Int" + ) + // Test concurrency + try testSystemSwiftInterface( + withTaskGroupRef, + ws: ws, + swiftInterfaceFile: "/_Concurrency.swiftinterface", + linePrefix: "@inlinable public func withTaskGroup" + ) + } + func testSwiftInterfaceAcrossModules() throws { guard let ws = try staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage") else { return } try ws.buildAndIndex()