Skip to content

Show unapplied function references in call hierarchy #1190

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Sources/SKTestSupport/SkipUnless.swift
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,34 @@ public enum SkipUnless {
}
}

/// Checks whether the index contains a fix that prevents it from adding relations to non-indexed locals
/// (https://github.com/apple/swift/pull/72930).
public static func indexOnlyHasContainedByRelationsToIndexedDecls(
file: StaticString = #file,
line: UInt = #line
) async throws {
return try await skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(6, 0), file: file, line: line) {
let project = try await IndexedSingleSwiftFileTestProject(
"""
func foo() {}

func 1️⃣testFunc(x: String) {
let myVar = foo
}
"""
)
let prepare = try await project.testClient.send(
CallHierarchyPrepareRequest(
textDocument: TextDocumentIdentifier(project.fileURI),
position: project.positions["1️⃣"]
)
)
let initialItem = try XCTUnwrap(prepare?.only)
let calls = try await project.testClient.send(CallHierarchyOutgoingCallsRequest(item: initialItem))
return calls != []
}
}

public static func longTestsEnabled() throws {
if let value = ProcessInfo.processInfo.environment["SKIP_LONG_TESTS"], value == "1" || value == "YES" {
throw XCTSkip("Long tests disabled using the `SKIP_LONG_TESTS` environment variable")
Expand Down
22 changes: 17 additions & 5 deletions Sources/SourceKitLSP/SourceKitLSPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2134,16 +2134,14 @@ extension SourceKitLSPServer {
return []
}
var callableUsrs = [data.usr]
// Calls to the accessors of a property count as calls to the property
callableUsrs += index.occurrences(relatedToUSR: data.usr, roles: .accessorOf).map(\.symbol.usr)
// Also show calls to the functions that this method overrides. This includes overridden class methods and
// satisfied protocol requirements.
callableUsrs += index.occurrences(ofUSR: data.usr, roles: .overrideOf).flatMap { occurrence in
occurrence.relations.filter { $0.roles.contains(.overrideOf) }.map(\.symbol.usr)
}
// callOccurrences are all the places that any of the USRs in callableUsrs is called.
// We also load the `calledBy` roles to get the method that contains the reference to this call.
let callOccurrences = callableUsrs.flatMap { index.occurrences(ofUSR: $0, roles: .calledBy) }
let callOccurrences = callableUsrs.flatMap { index.occurrences(ofUSR: $0, roles: .containedBy) }

// Maps functions that call a USR in `callableUSRs` to all the called occurrences of `callableUSRs` within the
// function. If a function `foo` calls `bar` multiple times, `callersToCalls[foo]` will contain two call
Expand All @@ -2154,7 +2152,7 @@ extension SourceKitLSPServer {
for call in callOccurrences {
// Callers are all `calledBy` relations of a call to a USR in `callableUsrs`, ie. all the functions that contain a
// call to a USR in callableUSRs. In practice, this should always be a single item.
let callers = call.relations.filter { $0.roles.contains(.calledBy) }.map(\.symbol)
let callers = call.relations.filter { $0.roles.contains(.containedBy) }.map(\.symbol)
for caller in callers {
callersToCalls[caller, default: []].append(call)
}
Expand Down Expand Up @@ -2190,8 +2188,11 @@ extension SourceKitLSPServer {
return []
}
let callableUsrs = [data.usr] + index.occurrences(relatedToUSR: data.usr, roles: .accessorOf).map(\.symbol.usr)
let callOccurrences = callableUsrs.flatMap { index.occurrences(relatedToUSR: $0, roles: .calledBy) }
let callOccurrences = callableUsrs.flatMap { index.occurrences(relatedToUSR: $0, roles: .containedBy) }
let calls = callOccurrences.compactMap { occurrence -> CallHierarchyOutgoingCall? in
guard occurrence.symbol.kind.isCallable else {
return nil
}
guard let location = indexToLSPLocation(occurrence.location) else {
return nil
}
Expand Down Expand Up @@ -2494,6 +2495,17 @@ extension IndexSymbolKind {
return .null
}
}

var isCallable: Bool {
switch self {
case .function, .instanceMethod, .classMethod, .staticMethod, .constructor, .destructor, .conversionFunction:
return true
case .unknown, .module, .namespace, .namespaceAlias, .macro, .enum, .struct, .protocol, .extension, .union,
.typealias, .field, .enumConstant, .parameter, .using, .concept, .commentTag, .variable, .instanceProperty,
.class, .staticProperty, .classProperty:
return false
}
}
}

extension SymbolOccurrence {
Expand Down
174 changes: 166 additions & 8 deletions Tests/SourceKitLSPTests/CallHierarchyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ final class CallHierarchyTests: XCTestCase {
}

func testIncomingCallHierarchyShowsSurroundingFunctionCall() async throws {
try await SkipUnless.indexOnlyHasContainedByRelationsToIndexedDecls()
// We used to show `myVar` as the caller here
let project = try await IndexedSingleSwiftFileTestProject(
"""
Expand All @@ -255,7 +256,6 @@ final class CallHierarchyTests: XCTestCase {
name: "testFunc(x:)",
kind: .function,
tags: nil,
detail: nil,
uri: project.fileURI,
range: Range(project.positions["2️⃣"]),
selectionRange: Range(project.positions["2️⃣"]),
Expand All @@ -271,6 +271,7 @@ final class CallHierarchyTests: XCTestCase {
}

func testIncomingCallHierarchyFromComputedProperty() async throws {
try await SkipUnless.indexOnlyHasContainedByRelationsToIndexedDecls()
let project = try await IndexedSingleSwiftFileTestProject(
"""
func 1️⃣foo() {}
Expand Down Expand Up @@ -303,7 +304,6 @@ final class CallHierarchyTests: XCTestCase {
name: "getter:testVar",
kind: .function,
tags: nil,
detail: nil,
uri: project.fileURI,
range: Range(project.positions["2️⃣"]),
selectionRange: Range(project.positions["2️⃣"]),
Expand All @@ -328,7 +328,6 @@ final class CallHierarchyTests: XCTestCase {
name: "testFunc()",
kind: .function,
tags: nil,
detail: nil,
uri: project.fileURI,
range: Range(project.positions["4️⃣"]),
selectionRange: Range(project.positions["4️⃣"]),
Expand Down Expand Up @@ -370,7 +369,6 @@ final class CallHierarchyTests: XCTestCase {
name: "testFunc()",
kind: .function,
tags: nil,
detail: nil,
uri: project.fileURI,
range: Range(project.positions["2️⃣"]),
selectionRange: Range(project.positions["2️⃣"]),
Expand Down Expand Up @@ -411,7 +409,6 @@ final class CallHierarchyTests: XCTestCase {
name: "getter:foo",
kind: .function,
tags: nil,
detail: nil,
uri: project.fileURI,
range: Range(project.positions["1️⃣"]),
selectionRange: Range(project.positions["1️⃣"]),
Expand Down Expand Up @@ -451,7 +448,6 @@ final class CallHierarchyTests: XCTestCase {
name: "testFunc()",
kind: .function,
tags: nil,
detail: nil,
uri: project.fileURI,
range: Range(project.positions["1️⃣"]),
selectionRange: Range(project.positions["1️⃣"]),
Expand Down Expand Up @@ -500,7 +496,6 @@ final class CallHierarchyTests: XCTestCase {
name: "test(proto:)",
kind: .function,
tags: nil,
detail: nil,
uri: project.fileURI,
range: Range(project.positions["2️⃣"]),
selectionRange: Range(project.positions["2️⃣"]),
Expand Down Expand Up @@ -549,7 +544,6 @@ final class CallHierarchyTests: XCTestCase {
name: "test(base:)",
kind: .function,
tags: nil,
detail: nil,
uri: project.fileURI,
range: Range(project.positions["2️⃣"]),
selectionRange: Range(project.positions["2️⃣"]),
Expand Down Expand Up @@ -606,4 +600,168 @@ final class CallHierarchyTests: XCTestCase {
]
)
}

func testUnappliedFunctionReferenceInIncomingCallHierarchy() async throws {
try await SkipUnless.indexOnlyHasContainedByRelationsToIndexedDecls()
let project = try await IndexedSingleSwiftFileTestProject(
"""
func 1️⃣foo() {}

func 2️⃣testFunc(x: String) {
let myVar = 3️⃣foo
}
"""
)
let prepare = try await project.testClient.send(
CallHierarchyPrepareRequest(
textDocument: TextDocumentIdentifier(project.fileURI),
position: project.positions["1️⃣"]
)
)
let initialItem = try XCTUnwrap(prepare?.only)
let calls = try await project.testClient.send(CallHierarchyIncomingCallsRequest(item: initialItem))
XCTAssertEqual(
calls,
[
CallHierarchyIncomingCall(
from: CallHierarchyItem(
name: "testFunc(x:)",
kind: .function,
tags: nil,
uri: project.fileURI,
range: Range(project.positions["2️⃣"]),
selectionRange: Range(project.positions["2️⃣"]),
data: .dictionary([
"usr": .string("s:4test0A4Func1xySS_tF"),
"uri": .string(project.fileURI.stringValue),
])
),
fromRanges: [Range(project.positions["3️⃣"])]
)
]
)
}

func testUnappliedFunctionReferenceInOutgoingCallHierarchy() async throws {
try await SkipUnless.indexOnlyHasContainedByRelationsToIndexedDecls()
let project = try await IndexedSingleSwiftFileTestProject(
"""
func 1️⃣foo() {}

func 2️⃣testFunc(x: String) {
let myVar = 3️⃣foo
}
"""
)
let prepare = try await project.testClient.send(
CallHierarchyPrepareRequest(
textDocument: TextDocumentIdentifier(project.fileURI),
position: project.positions["2️⃣"]
)
)
let initialItem = try XCTUnwrap(prepare?.only)
let calls = try await project.testClient.send(CallHierarchyOutgoingCallsRequest(item: initialItem))
XCTAssertEqual(
calls,
[
CallHierarchyOutgoingCall(
to: CallHierarchyItem(
name: "foo()",
kind: .function,
tags: nil,
uri: project.fileURI,
range: Range(project.positions["1️⃣"]),
selectionRange: Range(project.positions["1️⃣"]),
data: .dictionary([
"usr": .string("s:4test3fooyyF"),
"uri": .string(project.fileURI.stringValue),
])
),
fromRanges: [Range(project.positions["3️⃣"])]
)
]
)
}

func testIncomingCallHierarchyForPropertyInitializedWithClosure() async throws {
try await SkipUnless.indexOnlyHasContainedByRelationsToIndexedDecls()
let project = try await IndexedSingleSwiftFileTestProject(
"""
func 1️⃣foo() -> Int {}

let 2️⃣myVar: Int = {
3️⃣foo()
}()
"""
)
let prepare = try await project.testClient.send(
CallHierarchyPrepareRequest(
textDocument: TextDocumentIdentifier(project.fileURI),
position: project.positions["1️⃣"]
)
)
let initialItem = try XCTUnwrap(prepare?.only)
let calls = try await project.testClient.send(CallHierarchyIncomingCallsRequest(item: initialItem))
XCTAssertEqual(
calls,
[
CallHierarchyIncomingCall(
from: CallHierarchyItem(
name: "myVar",
kind: .variable,
tags: nil,
uri: project.fileURI,
range: Range(project.positions["2️⃣"]),
selectionRange: Range(project.positions["2️⃣"]),
data: .dictionary([
"usr": .string("s:4test5myVarSivp"),
"uri": .string(project.fileURI.stringValue),
])
),
fromRanges: [Range(project.positions["3️⃣"])]
)
]
)
}

func testOutgoingCallHierarchyForPropertyInitializedWithClosure() async throws {
try await SkipUnless.indexOnlyHasContainedByRelationsToIndexedDecls()
let project = try await IndexedSingleSwiftFileTestProject(
"""
func 1️⃣foo() -> Int {}

let 2️⃣myVar: Int = {
3️⃣foo()
}()
"""
)
let prepare = try await project.testClient.send(
CallHierarchyPrepareRequest(
textDocument: TextDocumentIdentifier(project.fileURI),
position: project.positions["2️⃣"]
)
)
let initialItem = try XCTUnwrap(prepare?.only)
let calls = try await project.testClient.send(CallHierarchyOutgoingCallsRequest(item: initialItem))
XCTAssertEqual(
calls,
[
CallHierarchyOutgoingCall(
to: CallHierarchyItem(
name: "foo()",
kind: .function,
tags: nil,
uri: project.fileURI,
range: Range(project.positions["1️⃣"]),
selectionRange: Range(project.positions["1️⃣"]),
data: .dictionary([
"usr": .string("s:4test3fooSiyF"),
"uri": .string(project.fileURI.stringValue),
])
),
fromRanges: [Range(project.positions["3️⃣"])]
)
]
)
}
}