mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-10-30 18:15:05 +01:00
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`.
500 lines
17 KiB
Swift
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)
|
|
}
|
|
}
|