Files
Rintaro Ishizaki a990227eb1 Locate binary module types in generated interfaces in type hierarchy
Type hierarchy items for types declared in a binary module, e.g.
`Equatable` among the supertypes of a type, pointed at the module's
`.swiftinterface` or `.swiftmodule` file, which a client can't open
in a useful way.

If the client supports `workspace/getReferenceDocument`, locate such
items in the module's generated interface, opened with the build
settings of a file that imports the module, as `workspace/symbol`
does. The item's range is the start of the interface, since the
symbol's position is only known once the interface is generated. A
client can resolve it with `workspaceSymbol/resolve`.
2026-10-26 14:57:09 -07:00

500 lines
17 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2022 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
//
//===----------------------------------------------------------------------===//
@_spi(SourceKitLSP) import LanguageServerProtocol
import SKLogging
import SKTestSupport
import SKUtilities
import SwiftExtensions
import TSCBasic
@_spi(SourceKitLSP) import ToolsProtocolsSwiftExtensions
import XCTest
final class TypeHierarchyTests: SourceKitLSPTestCase {
func testRootClassSupertypes() async throws {
let project = try await IndexedSingleSwiftFileTestProject(
"""
class 1️⃣MyClass {}
"""
)
let item = try await project.prepareTypeHierarchy(at: "1️⃣")
let supertypes = try await project.testClient.send(TypeHierarchySupertypesRequest(item: item))
assertEqualIgnoringData(supertypes, [])
}
func testSupertypesOfClass() async throws {
let project = try await IndexedSingleSwiftFileTestProject(
"""
class 1️⃣MyClass {}
protocol 2️⃣MyProtocol {}
class 3️⃣MySubclass: MyClass, MyProtocol {}
"""
)
let item = try await project.prepareTypeHierarchy(at: "3️⃣")
let supertypes = try await project.testClient.send(TypeHierarchySupertypesRequest(item: item))
assertEqualIgnoringData(
supertypes,
[
TypeHierarchyItem(name: "MyClass", kind: .class, location: "1️⃣", in: project),
TypeHierarchyItem(name: "MyProtocol", kind: .interface, location: "2️⃣", in: project),
]
)
}
func testConformedProtocolsOfStruct() async throws {
let project = try await IndexedSingleSwiftFileTestProject(
"""
protocol 1️⃣MyProtocol {}
protocol 2️⃣MyOtherProtocol {}
struct 3️⃣MyStruct: MyProtocol {}
extension MyStruct: MyOtherProtocol {}
"""
)
let item = try await project.prepareTypeHierarchy(at: "3️⃣")
let supertypes = try await project.testClient.send(TypeHierarchySupertypesRequest(item: item))
assertEqualIgnoringData(
supertypes,
[
TypeHierarchyItem(name: "MyOtherProtocol", kind: .interface, location: "2️⃣", in: project),
TypeHierarchyItem(name: "MyProtocol", kind: .interface, location: "1️⃣", in: project),
]
)
}
func testSubtypesOfClass() async throws {
let project = try await IndexedSingleSwiftFileTestProject(
"""
class MySuperclass {}
class 1️⃣MyClass: MySuperclass {}
class 2️⃣SubclassA: MyClass {}
class 3️⃣SubclassB: MyClass {}
"""
)
let item = try await project.prepareTypeHierarchy(at: "1️⃣")
let subtypes = try await project.testClient.send(TypeHierarchySubtypesRequest(item: item))
assertEqualIgnoringData(
subtypes,
[
TypeHierarchyItem(name: "SubclassA", kind: .class, location: "2️⃣", in: project),
TypeHierarchyItem(name: "SubclassB", kind: .class, location: "3️⃣", in: project),
]
)
}
func testProtocolConformancesAsSubtypes() async throws {
let project = try await IndexedSingleSwiftFileTestProject(
"""
protocol 1️⃣MyProtocol {}
class 2️⃣MyClass: MyProtocol {}
struct 3️⃣MyStruct: MyProtocol {}
enum 4️⃣MyEnum: MyProtocol {}
"""
)
let item = try await project.prepareTypeHierarchy(at: "1️⃣")
let subtypes = try await project.testClient.send(TypeHierarchySubtypesRequest(item: item))
assertEqualIgnoringData(
subtypes,
[
TypeHierarchyItem(name: "MyClass", kind: .class, location: "2️⃣", in: project),
TypeHierarchyItem(name: "MyEnum", kind: .enum, location: "4️⃣", in: project),
TypeHierarchyItem(name: "MyStruct", kind: .struct, location: "3️⃣", in: project),
]
)
}
func testOwnExtensionsAreNotSubtypes() async throws {
let project = try await IndexedSingleSwiftFileTestProject(
"""
protocol MyProtocol {}
enum 1️⃣MyEnum {}
extension MyEnum {
func foo() {}
}
extension MyEnum: MyProtocol {}
"""
)
let item = try await project.prepareTypeHierarchy(at: "1️⃣")
let subtypes = try await project.testClient.send(TypeHierarchySubtypesRequest(item: item))
assertEqualIgnoringData(subtypes, [])
}
func testRetroactiveConformancesAsSubtypes() async throws {
let project = try await IndexedSingleSwiftFileTestProject(
"""
protocol 1️⃣MyProtocol {}
enum Outer {
struct MyStruct {}
}
extension Outer.2️⃣MyStruct: MyProtocol {}
"""
)
let item = try await project.prepareTypeHierarchy(at: "1️⃣")
let response = try await project.testClient.send(TypeHierarchySubtypesRequest(item: item))
let subtypes = try XCTUnwrap(response)
// The item identifies the conforming type and is located at the extension that declares the conformance.
assertEqualIgnoringData(
subtypes,
[
TypeHierarchyItem(
name: "Outer.MyStruct",
kind: .struct,
detail: "Extension at test.swift:5",
location: "2️⃣",
in: project
)
]
)
XCTAssertEqual(subtypes.first?.sourceKitData?.usr, "s:4test5OuterO8MyStructV")
}
func testSubtypesOfRetroactiveConformance() async throws {
let project = try await IndexedSingleSwiftFileTestProject(
"""
protocol 1️⃣MyProtocol {}
class MyClass {}
extension MyClass: MyProtocol {}
class 2️⃣MySubclass: MyClass {}
"""
)
let item = try await project.prepareTypeHierarchy(at: "1️⃣")
let conformers = try await project.testClient.send(TypeHierarchySubtypesRequest(item: item))
let conformer = try XCTUnwrap(conformers?.only)
let subtypes = try await project.testClient.send(TypeHierarchySubtypesRequest(item: conformer))
assertEqualIgnoringData(
subtypes,
[TypeHierarchyItem(name: "MySubclass", kind: .class, location: "2️⃣", in: project)]
)
}
func testSubtypeDeclaredMoreThanOnceIsReportedOnce() async throws {
let project = try await SwiftPMTestProject(
files: [
"MyLibrary/include/empty.h": "",
"MyLibrary/Test.m": """
@protocol 1️⃣MyProtocol
@end
@interface 2️⃣MyClass <MyProtocol>
@end
@interface MyClass (MyCategory) <MyProtocol>
@end
""",
],
enableBackgroundIndexing: true
)
let (uri, positions) = try project.openDocument("Test.m")
let items = try await project.testClient.send(
TypeHierarchyPrepareRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
)
let item = try XCTUnwrap(items?.only)
// The class's own declaration is preferred over the category's.
let subtypes = try await project.testClient.send(TypeHierarchySubtypesRequest(item: item))
XCTAssertEqual(subtypes?.map(\.name), ["MyClass"])
XCTAssertEqual(subtypes?.map(\.kind), [.class])
XCTAssertEqual(subtypes?.map(\.range.lowerBound), [positions["2️⃣"]])
XCTAssertEqual(subtypes?.map(\.sourceKitData?.usr), ["c:objc(cs)MyClass"])
}
func testSupertypesFromCall() async throws {
let project = try await IndexedSingleSwiftFileTestProject(
"""
class 1️⃣MyClass {}
class MySubclass: MyClass {}
let x = 2️⃣MySubclass()
"""
)
let item = try await project.prepareTypeHierarchy(at: "2️⃣")
let supertypes = try await project.testClient.send(TypeHierarchySupertypesRequest(item: item))
assertEqualIgnoringData(
supertypes,
[
TypeHierarchyItem(name: "MyClass", kind: .class, location: "1️⃣", in: project)
]
)
}
func testTypeHierarchyForFileWithoutIndex() async throws {
let project = try await SwiftPMTestProject(files: [
"Test.swift": """
class MyClass {}
class 1️⃣MySubclass: MyClass {}
let x = MySubclass()
"""
])
let (uri, positions) = try project.openDocument("Test.swift")
// We don't have the type hierarchy because the file hasn't been indexed yet. Returning an empty array makes VS Code
// fail with the following two errors, so we should return `nil` instead.
// - MISSING provider
// - Cannot read properties of null (reading 'kind')
let response = try await project.testClient.send(
TypeHierarchyPrepareRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
)
XCTAssertNil(response)
}
func testNestedTypeNameInSubtypes() async throws {
let project = try await IndexedSingleSwiftFileTestProject(
"""
protocol 1️⃣MyProto {}
struct Outer {
struct 2️⃣Mid: MyProto {
struct 3️⃣Inner: MyProto {}
}
}
"""
)
let item = try await project.prepareTypeHierarchy(at: "1️⃣")
let subtypes = try await project.testClient.send(TypeHierarchySubtypesRequest(item: item))
assertEqualIgnoringData(
subtypes,
[
TypeHierarchyItem(name: "Outer.Mid", kind: .struct, location: "2️⃣", in: project),
TypeHierarchyItem(name: "Outer.Mid.Inner", kind: .struct, location: "3️⃣", in: project),
]
)
}
func testNestedTypeNameInSupertypes() async throws {
let project = try await IndexedSingleSwiftFileTestProject(
"""
class Outer {
class 1️⃣Mid {
class 2️⃣Inner: Mid {}
}
}
"""
)
let item = try await project.prepareTypeHierarchy(at: "2️⃣")
let supertypes = try await project.testClient.send(TypeHierarchySupertypesRequest(item: item))
assertEqualIgnoringData(
supertypes,
[
TypeHierarchyItem(name: "Outer.Mid", kind: .class, location: "1️⃣", in: project)
]
)
}
func testClangTypeHierarchy() async throws {
let project = try await SwiftPMTestProject(
files: [
"MyLibrary/include/empty.h": "",
"MyLibrary/Test.cpp": """
class Foo {};
class Bar: 1️⃣Foo {};
""",
],
enableBackgroundIndexing: true
)
let (uri, positions) = try project.openDocument("Test.cpp", language: .cpp)
let response = try await project.testClient.send(
TypeHierarchyPrepareRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
)
XCTAssertEqual(response?.count, 1)
}
func testClangTypeHierarchyInitiatedFromFunction() async throws {
let project = try await SwiftPMTestProject(
files: [
"MyLibrary/include/empty.h": "",
"MyLibrary/Test.cpp": """
void hello() {}
void test() {
1️⃣hello();
}
""",
],
enableBackgroundIndexing: true
)
let (uri, positions) = try project.openDocument("Test.cpp", language: .cpp)
let response = try await project.testClient.send(
TypeHierarchyPrepareRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
)
XCTAssertNil(response)
}
func testSystemSupertypeIsLocatedInGeneratedInterface() async throws {
let project = try await IndexedSingleSwiftFileTestProject(
"""
struct 1️⃣MyStruct: Equatable {}
""",
capabilities: ClientCapabilities(
experimental: [GetReferenceDocumentRequest.method: .dictionary(["supported": .bool(true)])]
),
indexSystemModules: true
)
let item = try await project.prepareTypeHierarchy(at: "1️⃣")
let supertypes = try await project.testClient.send(TypeHierarchySupertypesRequest(item: item))
let equatable = try XCTUnwrap(supertypes?.first { $0.name == "Equatable" })
// The item points at the start of the generated interface, since the position of the symbol in it is
// only known once the interface is generated.
let components = try XCTUnwrap(URLComponents(string: equatable.uri.arbitrarySchemeURL.absoluteString))
XCTAssertEqual(components.scheme, "sourcekit-lsp")
XCTAssertEqual(components.host, "generated-swift-interface")
XCTAssertEqual(equatable.range, Range(Position(line: 0, utf16index: 0)))
// `workspaceSymbol/resolve` finds the position of the symbol from the item's URI and data.
let resolved = try await project.testClient.send(
WorkspaceSymbolResolveRequest(
workspaceSymbol: WorkspaceSymbol(
name: equatable.name,
kind: equatable.kind,
location: .uri(.init(uri: equatable.uri)),
data: equatable.data
)
)
)
guard case .location(let location) = resolved.location else {
XCTFail("Expected .location after resolve, got \(resolved.location)")
return
}
let referenceDocument = try await project.testClient.send(GetReferenceDocumentRequest(uri: location.uri))
let resolvedLine = try XCTUnwrap(LineTable(referenceDocument.content).line(at: location.range.lowerBound.line))
XCTAssert(
resolvedLine.contains("protocol Equatable"),
"Line at the resolved position should declare `Equatable`, got: '\(resolvedLine)'"
)
// The item's data selects the workspace through the generated interface URI.
let subtypes = try await project.testClient.send(TypeHierarchySubtypesRequest(item: equatable))
XCTAssert(
subtypes?.contains { $0.name == "MyStruct" } ?? false,
"Expected `MyStruct` among the subtypes of `Equatable`, got \(String(describing: subtypes?.map(\.name)))"
)
}
func testSystemSupertypeWithoutGeneratedInterfaceSupport() async throws {
let project = try await IndexedSingleSwiftFileTestProject(
"""
struct 1️⃣MyStruct: Equatable {}
""",
indexSystemModules: true
)
let item = try await project.prepareTypeHierarchy(at: "1️⃣")
let supertypes = try await project.testClient.send(TypeHierarchySupertypesRequest(item: item))
let equatable = try XCTUnwrap(supertypes?.first { $0.name == "Equatable" })
// Without generated-interface support, the item points at the module's interface file.
let path = try XCTUnwrap(equatable.uri.fileURL?.path)
XCTAssert(
path.hasSuffix(".swiftinterface") || path.hasSuffix(".swiftmodule"),
"Expected the module's interface file, got \(path)"
)
}
func testSupertypesOfClientConstructedItem() async throws {
let project = try await IndexedSingleSwiftFileTestProject(
"""
class 1️⃣MyClass {}
class MySubclass: MyClass {}
"""
)
// Only `data` identifies the symbol; the remaining fields are placeholders.
var item = TypeHierarchyItem(
name: "",
kind: .null,
tags: nil,
uri: project.fileURI,
range: Position(line: 0, utf16index: 0)..<Position(line: 0, utf16index: 0),
selectionRange: Position(line: 0, utf16index: 0)..<Position(line: 0, utf16index: 0)
)
item.sourceKitData = SourceKitSymbolIdentifier(usr: "s:4test10MySubclassC", uri: project.fileURI)
let supertypes = try await project.testClient.send(TypeHierarchySupertypesRequest(item: item))
assertEqualIgnoringData(
supertypes,
[TypeHierarchyItem(name: "MyClass", kind: .class, location: "1️⃣", in: project)]
)
}
}
// MARK: - Utilities
fileprivate extension TypeHierarchyItem {
var withoutData: TypeHierarchyItem {
var item = self
item.data = nil
return item
}
}
/// Compares the given type hierarchies ignoring the implementation-specific
/// data field (which includes e.g. USRs that are difficult to test, especially
/// in the presence of extensions, and are not user-visible anyway).
private func assertEqualIgnoringData(
_ actual: [TypeHierarchyItem]?,
_ expected: [TypeHierarchyItem],
file: StaticString = #filePath,
line: UInt = #line
) {
guard let actual else {
XCTFail("Expected non-nil type hierarchy", file: file, line: line)
return
}
XCTAssertEqual(
actual.map(\.withoutData),
expected.map(\.withoutData),
file: file,
line: line
)
}
fileprivate extension TypeHierarchyItem {
init(
name: String,
kind: SymbolKind,
detail: String = "test",
location locationMarker: String,
in ws: IndexedSingleSwiftFileTestProject
) {
self.init(
name: name,
kind: kind,
tags: nil,
detail: detail,
uri: ws.fileURI,
range: Range(ws.positions[locationMarker]),
selectionRange: Range(ws.positions[locationMarker])
)
}
}
fileprivate extension IndexedSingleSwiftFileTestProject {
func prepareTypeHierarchy(at locationMarker: String, line: UInt = #line) async throws -> TypeHierarchyItem {
let items = try await testClient.send(
TypeHierarchyPrepareRequest(
textDocument: TextDocumentIdentifier(self.fileURI),
position: self.positions[locationMarker]
)
)
XCTAssertEqual(items?.count, 1, "Expected exactly one item from the type hierarchy preapre", line: line)
return try XCTUnwrap(items?.first, line: line)
}
}