Skip to content

Commit 440dc62

Browse files
committed
Fix jump-to-definition to methods in swift interfaces that are in synthesized extensions
For example when trying to go-to-definition to `filter` on `Array`, we get a USR `s:s14_ArrayProtocolPsE6filterySay7ElementQzGSbAEKXEKF::SYNTHESIZED::s:Sa`. We were trying to look it up in the index, which failed because synthesized extension methods are not indexed. Instead, consult the `module` and `groupName` that `sourcekitd` returns in the cursor info request to decide which module to jump to. rdar://126240558
1 parent 5a5b501 commit 440dc62

File tree

6 files changed

+143
-66
lines changed

6 files changed

+143
-66
lines changed

Sources/LanguageServerProtocol/Requests/OpenInterfaceRequest.swift

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,24 @@ public struct OpenInterfaceRequest: TextDocumentRequest, Hashable {
2323
public var moduleName: String
2424

2525
/// The module group name.
26-
public var groupNames: [String]
26+
public var groupName: String?
2727

2828
/// The symbol USR to search for in the generated module interface.
2929
public var symbolUSR: String?
3030

31-
public init(textDocument: TextDocumentIdentifier, name: String, symbolUSR: String?) {
31+
public init(textDocument: TextDocumentIdentifier, name: String, groupName: String?, symbolUSR: String?) {
3232
self.textDocument = textDocument
3333
self.symbolUSR = symbolUSR
34-
// Stdlib Swift modules are all in the "Swift" module, but their symbols return a module name `Swift.***`.
35-
let splitName = name.split(separator: ".")
36-
self.moduleName = String(splitName[0])
37-
self.groupNames = [String.SubSequence](splitName.dropFirst()).map(String.init)
34+
self.moduleName = name
35+
self.groupName = groupName
3836
}
3937

4038
/// Name of interface module name with group names appended
4139
public var name: String {
42-
if groupNames.count > 0 {
43-
return "\(self.moduleName).\(self.groupNames.joined(separator: "."))"
44-
} else {
45-
return self.moduleName
40+
if let groupName {
41+
return "\(self.moduleName).\(groupName.replacing("/", with: "."))"
4642
}
43+
return self.moduleName
4744
}
4845
}
4946

Sources/LanguageServerProtocol/Requests/SymbolInfoRequest.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ public struct SymbolInfoRequest: TextDocumentRequest, Hashable {
4848
/// Detailed information about a symbol, such as the response to a `SymbolInfoRequest`
4949
/// **(LSP Extension)**.
5050
public struct SymbolDetails: ResponseType, Hashable {
51+
public struct ModuleInfo: Codable, Hashable, Sendable {
52+
/// The name of the module in which the symbol is defined.
53+
public let moduleName: String
54+
55+
/// If the symbol is defined within a subgroup of a module, the name of the group. Otherwise `nil`.
56+
public let groupName: String?
57+
58+
public init(moduleName: String, groupName: String? = nil) {
59+
self.moduleName = moduleName
60+
self.groupName = groupName
61+
}
62+
}
5163

5264
/// The name of the symbol, if any.
5365
public var name: String?
@@ -87,6 +99,11 @@ public struct SymbolDetails: ResponseType, Hashable {
8799
/// Optional because `clangd` does not return whether a symbol is dynamic.
88100
public var isDynamic: Bool?
89101

102+
/// Whether this symbol is defined in the SDK or standard library.
103+
///
104+
/// This property only applies to Swift symbols.
105+
public var isSystem: Bool?
106+
90107
/// If the symbol is dynamic, the USRs of the types that might be called.
91108
///
92109
/// This is relevant in the following cases
@@ -112,21 +129,31 @@ public struct SymbolDetails: ResponseType, Hashable {
112129
/// `B` may be called dynamically.
113130
public var receiverUsrs: [String]?
114131

132+
/// If the symbol is defined in a module that doesn't have source information associated with it, the name and group
133+
/// and group name that defines this symbol.
134+
///
135+
/// This property only applies to Swift symbols.
136+
public var systemModule: ModuleInfo?
137+
115138
public init(
116139
name: String?,
117140
containerName: String?,
118141
usr: String?,
119142
bestLocalDeclaration: Location?,
120143
kind: SymbolKind?,
121144
isDynamic: Bool?,
122-
receiverUsrs: [String]?
145+
isSystem: Bool?,
146+
receiverUsrs: [String]?,
147+
systemModule: ModuleInfo?
123148
) {
124149
self.name = name
125150
self.containerName = containerName
126151
self.usr = usr
127152
self.bestLocalDeclaration = bestLocalDeclaration
128153
self.kind = kind
129154
self.isDynamic = isDynamic
155+
self.isSystem = isSystem
130156
self.receiverUsrs = receiverUsrs
157+
self.systemModule = systemModule
131158
}
132159
}

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1842,13 +1842,25 @@ extension SourceKitLSPServer {
18421842
if symbol.kind == .module, let name = symbol.name {
18431843
let interfaceLocation = try await self.definitionInInterface(
18441844
moduleName: name,
1845+
groupName: nil,
18451846
symbolUSR: nil,
18461847
originatorUri: uri,
18471848
languageService: languageService
18481849
)
18491850
return [interfaceLocation]
18501851
}
18511852

1853+
if symbol.isSystem ?? false, let systemModule = symbol.systemModule {
1854+
let location = try await self.definitionInInterface(
1855+
moduleName: systemModule.moduleName,
1856+
groupName: systemModule.groupName,
1857+
symbolUSR: symbol.usr,
1858+
originatorUri: uri,
1859+
languageService: languageService
1860+
)
1861+
return [location]
1862+
}
1863+
18521864
guard let index = await self.workspaceForDocument(uri: uri)?.index else {
18531865
if let bestLocalDeclaration = symbol.bestLocalDeclaration {
18541866
return [bestLocalDeclaration]
@@ -1891,19 +1903,7 @@ extension SourceKitLSPServer {
18911903
return [bestLocalDeclaration]
18921904
}
18931905

1894-
return try await occurrences.asyncCompactMap { occurrence in
1895-
if URL(fileURLWithPath: occurrence.location.path).pathExtension == "swiftinterface" {
1896-
// If the location is in `.swiftinterface` file, use moduleName to return textual interface.
1897-
return try await self.definitionInInterface(
1898-
moduleName: occurrence.location.moduleName,
1899-
symbolUSR: occurrence.symbol.usr,
1900-
originatorUri: uri,
1901-
languageService: languageService
1902-
)
1903-
}
1904-
return indexToLSPLocation(occurrence.location)
1905-
}
1906-
.sorted()
1906+
return occurrences.compactMap { indexToLSPLocation($0.location) }.sorted()
19071907
}
19081908

19091909
/// Returns the result of a `DefinitionRequest` by running a `SymbolInfoRequest`, inspecting
@@ -1968,13 +1968,15 @@ extension SourceKitLSPServer {
19681968
/// compiler arguments to generate the generated interface.
19691969
func definitionInInterface(
19701970
moduleName: String,
1971+
groupName: String?,
19711972
symbolUSR: String?,
19721973
originatorUri: DocumentURI,
19731974
languageService: LanguageService
19741975
) async throws -> Location {
19751976
let openInterface = OpenInterfaceRequest(
19761977
textDocument: TextDocumentIdentifier(originatorUri),
19771978
name: moduleName,
1979+
groupName: groupName,
19781980
symbolUSR: symbolUSR
19791981
)
19801982
guard let interfaceDetails = try await languageService.openInterface(openInterface) else {

Sources/SourceKitLSP/Swift/CursorInfo.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ struct CursorInfo {
6565
return nil
6666
}
6767

68-
var location: Location? = nil
68+
let location: Location?
6969
if let filepath: String = dict[keys.filePath],
7070
let line: Int = dict[keys.line],
7171
let column: Int = dict[keys.column]
@@ -76,6 +76,16 @@ struct CursorInfo {
7676
utf16index: column - 1
7777
)
7878
location = Location(uri: DocumentURI(URL(fileURLWithPath: filepath)), range: Range(position))
79+
} else {
80+
location = nil
81+
}
82+
83+
let module: SymbolDetails.ModuleInfo?
84+
if let moduleName: String = dict[keys.moduleName] {
85+
let groupName: String? = dict[keys.groupName]
86+
module = SymbolDetails.ModuleInfo(moduleName: moduleName, groupName: groupName)
87+
} else {
88+
module = nil
7989
}
8090

8191
self.init(
@@ -86,7 +96,9 @@ struct CursorInfo {
8696
bestLocalDeclaration: location,
8797
kind: kind.asSymbolKind(sourcekitd.values),
8898
isDynamic: dict[keys.isDynamic] ?? false,
89-
receiverUsrs: dict[keys.receivers]?.compactMap { $0[keys.usr] as String? } ?? []
99+
isSystem: dict[keys.isSystem] ?? false,
100+
receiverUsrs: dict[keys.receivers]?.compactMap { $0[keys.usr] as String? } ?? [],
101+
systemModule: module
90102
),
91103
annotatedDeclaration: dict[keys.annotatedDecl],
92104
documentationXML: dict[keys.docFullAsXML],

Sources/SourceKitLSP/Swift/OpenInterface.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ extension SwiftLanguageService {
7373
let skreq = sourcekitd.dictionary([
7474
keys.request: requests.editorOpenInterface,
7575
keys.moduleName: name,
76-
keys.groupName: request.groupNames.isEmpty ? nil : request.groupNames as [SKDRequestValue],
76+
keys.groupName: request.groupName,
7777
keys.name: interfaceURI.pseudoPath,
7878
keys.synthesizedExtension: 1,
7979
keys.compilerArgs: await self.buildSettings(for: uri)?.compilerArgs as [SKDRequestValue]?,

Tests/SourceKitLSPTests/SwiftInterfaceTests.swift

Lines changed: 78 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,7 @@ final class SwiftInterfaceTests: XCTestCase {
4444
XCTFail("Unexpected response: \(resp)")
4545
return
4646
}
47-
XCTAssertEqual(locations.count, 1)
48-
let location = try XCTUnwrap(locations.first)
47+
let location = try XCTUnwrap(locations.only)
4948
XCTAssertTrue(location.uri.pseudoPath.hasSuffix("/Foundation.swiftinterface"))
5049
let fileContents = try XCTUnwrap(location.uri.fileURL.flatMap({ try String(contentsOf: $0, encoding: .utf8) }))
5150
// Sanity-check that the generated Swift Interface contains Swift code
@@ -87,6 +86,7 @@ final class SwiftInterfaceTests: XCTestCase {
8786
let openInterface = OpenInterfaceRequest(
8887
textDocument: TextDocumentIdentifier(mainUri),
8988
name: "MyLibrary",
89+
groupName: nil,
9090
symbolUSR: nil
9191
)
9292
let interfaceDetails = try unwrap(await project.testClient.send(openInterface))
@@ -108,38 +108,6 @@ final class SwiftInterfaceTests: XCTestCase {
108108
)
109109
}
110110

111-
/// Used by testDefinitionInSystemModuleInterface
112-
private func testSystemSwiftInterface(
113-
uri: DocumentURI,
114-
position: Position,
115-
testClient: TestSourceKitLSPClient,
116-
swiftInterfaceFile: String,
117-
linePrefix: String,
118-
line: UInt = #line
119-
) async throws {
120-
let definition = try await testClient.send(
121-
DefinitionRequest(
122-
textDocument: TextDocumentIdentifier(uri),
123-
position: position
124-
)
125-
)
126-
guard case .locations(let jump) = definition else {
127-
XCTFail("Response is not locations", line: line)
128-
return
129-
}
130-
let location = try XCTUnwrap(jump.first)
131-
XCTAssertTrue(
132-
location.uri.pseudoPath.hasSuffix(swiftInterfaceFile),
133-
"Path was: '\(location.uri.pseudoPath)'",
134-
line: line
135-
)
136-
// load contents of swiftinterface
137-
let contents = try XCTUnwrap(location.uri.fileURL.flatMap({ try String(contentsOf: $0, encoding: .utf8) }))
138-
let lineTable = LineTable(contents)
139-
let destinationLine = lineTable[location.range.lowerBound.line]
140-
XCTAssert(destinationLine.hasPrefix(linePrefix), "Full line was: '\(destinationLine)'", line: line)
141-
}
142-
143111
func testDefinitionInSystemModuleInterface() async throws {
144112
let project = try await IndexedSingleSwiftFileTestProject(
145113
"""
@@ -158,23 +126,23 @@ final class SwiftInterfaceTests: XCTestCase {
158126
)
159127

160128
// Test stdlib with one submodule
161-
try await testSystemSwiftInterface(
129+
try await assertSystemSwiftInterface(
162130
uri: project.fileURI,
163131
position: project.positions["1️⃣"],
164132
testClient: project.testClient,
165133
swiftInterfaceFile: "/Swift.String.swiftinterface",
166134
linePrefix: "@frozen public struct String"
167135
)
168136
// Test stdlib with two submodules
169-
try await testSystemSwiftInterface(
137+
try await assertSystemSwiftInterface(
170138
uri: project.fileURI,
171139
position: project.positions["2️⃣"],
172140
testClient: project.testClient,
173141
swiftInterfaceFile: "/Swift.Math.Integers.swiftinterface",
174142
linePrefix: "@frozen public struct Int"
175143
)
176144
// Test concurrency
177-
try await testSystemSwiftInterface(
145+
try await assertSystemSwiftInterface(
178146
uri: project.fileURI,
179147
position: project.positions["3️⃣"],
180148
testClient: project.testClient,
@@ -224,8 +192,7 @@ final class SwiftInterfaceTests: XCTestCase {
224192
XCTFail("Unexpected response: \(resp)")
225193
return
226194
}
227-
XCTAssertEqual(locations.count, 1)
228-
let location = try XCTUnwrap(locations.first)
195+
let location = try XCTUnwrap(locations.only)
229196
XCTAssertTrue(location.uri.pseudoPath.hasSuffix("/MyLibrary.swiftinterface"))
230197
let fileContents = try XCTUnwrap(location.uri.fileURL.flatMap({ try String(contentsOf: $0, encoding: .utf8) }))
231198
XCTAssertTrue(
@@ -242,4 +209,76 @@ final class SwiftInterfaceTests: XCTestCase {
242209
"Generated interface did not contain expected text.\n\(fileContents)"
243210
)
244211
}
212+
213+
func testJumpToSynthesizedExtensionMethodInSystemModuleWithoutIndex() async throws {
214+
let testClient = try await TestSourceKitLSPClient()
215+
let uri = DocumentURI.for(.swift)
216+
217+
let positions = testClient.openDocument(
218+
"""
219+
func test(x: [String]) {
220+
let rows = x.1️⃣filter { !$0.isEmpty }
221+
}
222+
""",
223+
uri: uri
224+
)
225+
226+
try await assertSystemSwiftInterface(
227+
uri: uri,
228+
position: positions["1️⃣"],
229+
testClient: testClient,
230+
swiftInterfaceFile: "/Swift.Collection.Array.swiftinterface",
231+
linePrefix: "@inlinable public func filter(_ isIncluded: (Element) throws -> Bool) rethrows -> [Element]"
232+
)
233+
}
234+
235+
func testJumpToSynthesizedExtensionMethodInSystemModuleWithIndex() async throws {
236+
let project = try await IndexedSingleSwiftFileTestProject(
237+
"""
238+
func test(x: [String]) {
239+
let rows = x.1️⃣filter { !$0.isEmpty }
240+
}
241+
""",
242+
indexSystemModules: true
243+
)
244+
245+
try await assertSystemSwiftInterface(
246+
uri: project.fileURI,
247+
position: project.positions["1️⃣"],
248+
testClient: project.testClient,
249+
swiftInterfaceFile: "/Swift.Collection.Array.swiftinterface",
250+
linePrefix: "@inlinable public func filter(_ isIncluded: (Element) throws -> Bool) rethrows -> [Element]"
251+
)
252+
}
253+
}
254+
255+
private func assertSystemSwiftInterface(
256+
uri: DocumentURI,
257+
position: Position,
258+
testClient: TestSourceKitLSPClient,
259+
swiftInterfaceFile: String,
260+
linePrefix: String,
261+
line: UInt = #line
262+
) async throws {
263+
let definition = try await testClient.send(
264+
DefinitionRequest(
265+
textDocument: TextDocumentIdentifier(uri),
266+
position: position
267+
)
268+
)
269+
guard case .locations(let jump) = definition else {
270+
XCTFail("Response is not locations", line: line)
271+
return
272+
}
273+
let location = try XCTUnwrap(jump.only)
274+
XCTAssertTrue(
275+
location.uri.pseudoPath.hasSuffix(swiftInterfaceFile),
276+
"Path was: '\(location.uri.pseudoPath)'",
277+
line: line
278+
)
279+
// load contents of swiftinterface
280+
let contents = try XCTUnwrap(location.uri.fileURL.flatMap({ try String(contentsOf: $0, encoding: .utf8) }))
281+
let lineTable = LineTable(contents)
282+
let destinationLine = lineTable[location.range.lowerBound.line].trimmingCharacters(in: .whitespaces)
283+
XCTAssert(destinationLine.hasPrefix(linePrefix), "Full line was: '\(destinationLine)'", line: line)
245284
}

0 commit comments

Comments
 (0)