Skip to content

Commit b2b66ca

Browse files
committed
Support rename of functions across class hiearchies and protocols
rdar://123536502
1 parent 6f94d86 commit b2b66ca

File tree

4 files changed

+313
-16
lines changed

4 files changed

+313
-16
lines changed

Sources/SourceKitLSP/Rename.swift

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,37 @@ extension SourceKitServer {
623623
}
624624
}
625625

626+
/// Starting from the given USR, compute the transitive closure of all declarations that are overridden or override
627+
/// the symbol, including the USR itself.
628+
///
629+
/// This includes symbols that need to traverse the inheritance hierarchy up and down. For example, it includes all
630+
/// occurrences of `foo` in the following when started from `Inherited.foo`.
631+
///
632+
/// ```swift
633+
/// class Base { func foo() {} }
634+
/// class Inherited: Base { override func foo() {} }
635+
/// class OtherInherited: Base { override func foo() {} }
636+
/// ```
637+
private func overridingAndOverriddenUsrs(of usr: String, index: IndexStoreDB) -> [String] {
638+
var workList = [usr]
639+
var usrs: [String] = []
640+
while let usr = workList.popLast() {
641+
usrs.append(usr)
642+
var relatedUsrs = index.occurrences(relatedToUSR: usr, roles: .overrideOf).map(\.symbol.usr)
643+
relatedUsrs += index.occurrences(ofUSR: usr, roles: .overrideOf).flatMap { occurrence in
644+
occurrence.relations.filter { $0.roles.contains(.overrideOf) }.map(\.symbol.usr)
645+
}
646+
for overriddenUsr in relatedUsrs {
647+
if usrs.contains(overriddenUsr) || workList.contains(overriddenUsr) {
648+
// Already handling this USR. Nothing to do.
649+
continue
650+
}
651+
workList.append(overriddenUsr)
652+
}
653+
}
654+
return usrs
655+
}
656+
626657
func rename(_ request: RenameRequest) async throws -> WorkspaceEdit? {
627658
let uri = request.textDocument.uri
628659
let snapshot = try documentManager.latestSnapshot(uri)
@@ -669,8 +700,46 @@ extension SourceKitServer {
669700
// If we have a USR + old name, perform an index lookup to find workspace-wide symbols to rename.
670701
// First, group all occurrences of that USR by the files they occur in.
671702
var locationsByFile: [URL: [RenameLocation]] = [:]
672-
for occurrence in index.occurrences(ofUSR: usr, roles: renameRoles) {
703+
704+
var languageServerTypesCache: [URL: LanguageServerType?] = [:]
705+
func languageServerType(of url: URL) -> LanguageServerType? {
706+
if let cachedValue = languageServerTypesCache[url] {
707+
return cachedValue
708+
}
709+
let serverType = LanguageServerType(symbolProvider: index.symbolProvider(for: url.path))
710+
languageServerTypesCache[url] = serverType
711+
return serverType
712+
}
713+
714+
let usrsToRename = overridingAndOverriddenUsrs(of: usr, index: index)
715+
let occurrencesToRename = usrsToRename.flatMap { index.occurrences(ofUSR: $0, roles: renameRoles) }
716+
for occurrence in occurrencesToRename {
673717
let url = URL(fileURLWithPath: occurrence.location.path)
718+
719+
// Determine whether we should add the location produced by the index to those that will be renamed, or if it has
720+
// already been handled by the set provided by the AST.
721+
if changes[DocumentURI(url)] != nil {
722+
if occurrence.symbol.usr == usr {
723+
// If the language server's rename function already produced AST-based locations for this symbol, no need to
724+
// perform an indexed rename for it.
725+
continue
726+
}
727+
switch languageServerType(of: url) {
728+
case .swift:
729+
// sourcekitd only produces AST-based results for the direct calls to this USR. This is because the Swift
730+
// AST only has upwards references to superclasses and overridden methods, not the other way round. It is
731+
// thus not possible to (easily) compute an up-down closure like described in `overridingAndOverriddenUsrs`.
732+
// We thus need to perform an indexed rename for other, related USRs.
733+
break
734+
case .clangd:
735+
// clangd produces AST-based results for the entire class hierarchy, so nothing to do.
736+
continue
737+
case nil:
738+
// Unknown symbol provider
739+
continue
740+
}
741+
}
742+
674743
let renameLocation = RenameLocation(
675744
line: occurrence.location.line,
676745
utf8Column: occurrence.location.utf8Column,
@@ -683,12 +752,11 @@ extension SourceKitServer {
683752
// edits.
684753
let urisAndEdits =
685754
await locationsByFile
686-
.filter { changes[DocumentURI($0.key)] == nil }
687755
.concurrentMap { (url: URL, renameLocations: [RenameLocation]) -> (DocumentURI, [TextEdit])? in
688756
let uri = DocumentURI(url)
689757
let language: Language
690-
switch index.symbolProvider(for: url.path) {
691-
case .clang:
758+
switch languageServerType(of: url) {
759+
case .clangd:
692760
// Technically, we still don't know the language of the source file but defaulting to C is sufficient to
693761
// ensure we get the clang toolchain language server, which is all we care about.
694762
language = .c
@@ -728,12 +796,8 @@ extension SourceKitServer {
728796
return (uri, edits)
729797
}.compactMap { $0 }
730798
for (uri, editsForUri) in urisAndEdits {
731-
precondition(
732-
changes[uri] == nil,
733-
"We should have only computed edits for URIs that didn't have edits from the initial rename request"
734-
)
735799
if !editsForUri.isEmpty {
736-
changes[uri] = editsForUri
800+
changes[uri, default: []] += editsForUri
737801
}
738802
}
739803
var edits = renameResult.edits
@@ -754,15 +818,19 @@ extension SourceKitServer {
754818
guard
755819
let index = workspace.index,
756820
let usr = languageServicePrepareRename.usr,
757-
let oldName = try await self.getCrossLanguageName(forUsr: usr, workspace: workspace, index: index)
821+
let oldName = try await self.getCrossLanguageName(forUsr: usr, workspace: workspace, index: index),
822+
var definitionName = oldName.definitionName
758823
else {
759824
return prepareRenameResult
760825
}
826+
if oldName.definitionLanguage == .swift, definitionName.hasSuffix("()") {
827+
definitionName = String(definitionName.dropLast(2))
828+
}
761829

762830
// Get the name of the symbol's definition, if possible.
763831
// This is necessary for cross-language rename. Eg. when renaming an Objective-C method from Swift,
764832
// the user still needs to enter the new Objective-C name.
765-
prepareRenameResult.placeholder = oldName.definitionName
833+
prepareRenameResult.placeholder = definitionName
766834
return prepareRenameResult
767835
}
768836

Sources/SourceKitLSP/SourceKitServer.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ enum LanguageServerType: Hashable {
4848
}
4949
}
5050

51+
init?(symbolProvider: SymbolProviderKind?) {
52+
switch symbolProvider {
53+
case .clang: self = .clangd
54+
case .swift: self = .swift
55+
case nil: return nil
56+
}
57+
}
58+
5159
/// The `ToolchainLanguageServer` class used to provide functionality for this language class.
5260
var serverType: ToolchainLanguageServer.Type {
5361
switch self {

Tests/SourceKitLSPTests/RenameAssertions.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ private func apply(edits: [TextEdit], to source: String) -> String {
3434
/// Test that applying the edits returned from the requests always result in `expected`.
3535
func assertSingleFileRename(
3636
_ markedSource: String,
37-
language: Language? = nil,
37+
language: Language = .swift,
3838
newName: String,
3939
expectedPrepareRenamePlaceholder: String,
4040
expected: String,
@@ -44,7 +44,7 @@ func assertSingleFileRename(
4444
) async throws {
4545
try await SkipUnless.sourcekitdSupportsRename()
4646
let testClient = try await TestSourceKitLSPClient()
47-
let uri = DocumentURI.for(language ?? .swift, testName: testName)
47+
let uri = DocumentURI.for(language, testName: testName)
4848
let positions = testClient.openDocument(markedSource, uri: uri, language: language)
4949
guard !positions.allMarkers.isEmpty else {
5050
XCTFail("Test case did not contain any markers at which to invoke the rename", file: file, line: line)

0 commit comments

Comments
 (0)