Skip to content

Commit fa768f5

Browse files
committed
Add tests to rename C++ symbols exposed to Swift
This exposes an issue where we wouldn’t rename a symbol on the clang side f a symbol was only defined in a header and not a C/C++/... file. In that case we failed to determine the language of the header file because there was no unit file that contained the header file. The cleaner solution here is to ask for the symbol provider of each occurrence directly. rdar://127391127
1 parent 0e0594b commit fa768f5

File tree

4 files changed

+175
-44
lines changed

4 files changed

+175
-44
lines changed

Sources/SKTestSupport/SwiftPMTestProject.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ public class SwiftPMTestProject: MultiFileTestProject {
177177
if !manifest.contains("swift-tools-version") {
178178
// Tests specify a shorthand package manifest that doesn't contain the tools version boilerplate.
179179
manifest = """
180-
// swift-tools-version: 5.7
180+
// swift-tools-version: 5.10
181181
182182
import PackageDescription
183183

Sources/SemanticIndex/CheckedIndex.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,6 @@ public struct UncheckedIndex: Sendable {
176176
return CheckedIndex(index: underlyingIndexStoreDB, checkLevel: checkLevel)
177177
}
178178

179-
public func symbolProvider(for sourceFilePath: String) -> SymbolProviderKind? {
180-
return underlyingIndexStoreDB.symbolProvider(for: sourceFilePath)
181-
}
182-
183179
/// Wait for IndexStoreDB to be updated based on new unit files written to disk.
184180
public func pollForUnitChangesAndWait() {
185181
self.underlyingIndexStoreDB.pollForUnitChangesAndWait()

Sources/SourceKitLSP/Rename.swift

Lines changed: 28 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ extension SourceKitLSPServer {
500500
) async -> (swiftLanguageService: SwiftLanguageService, snapshot: DocumentSnapshot, location: SymbolLocation)? {
501501
var reference: SymbolOccurrence? = nil
502502
index.forEachSymbolOccurrence(byUSR: usr, roles: renameRoles) {
503-
if index.unchecked.symbolProvider(for: $0.location.path) == .swift {
503+
if $0.symbolProvider == .swift {
504504
reference = $0
505505
// We have found a reference from Swift. Stop iteration.
506506
return false
@@ -631,7 +631,7 @@ extension SourceKitLSPServer {
631631
// If we terminate early by returning `false` from the closure, `forEachSymbolOccurrence` returns `true`,
632632
// indicating that we have found a reference from clang.
633633
let hasReferenceFromClang = !index.forEachSymbolOccurrence(byUSR: usr, roles: renameRoles) {
634-
return index.unchecked.symbolProvider(for: $0.location.path) != .clang
634+
return $0.symbolProvider != .clang
635635
}
636636
let clangName: String?
637637
if hasReferenceFromClang {
@@ -730,29 +730,7 @@ extension SourceKitLSPServer {
730730

731731
// If we have a USR + old name, perform an index lookup to find workspace-wide symbols to rename.
732732
// First, group all occurrences of that USR by the files they occur in.
733-
var locationsByFile: [URL: [RenameLocation]] = [:]
734-
735-
actor LanguageServerTypesCache {
736-
let index: UncheckedIndex
737-
var languageServerTypesCache: [URL: LanguageServerType?] = [:]
738-
739-
init(index: UncheckedIndex) {
740-
self.index = index
741-
}
742-
743-
func languageServerType(for url: URL) -> LanguageServerType? {
744-
if let cachedValue = languageServerTypesCache[url] {
745-
return cachedValue
746-
}
747-
let serverType = LanguageServerType(
748-
symbolProvider: index.symbolProvider(for: url.path)
749-
)
750-
languageServerTypesCache[url] = serverType
751-
return serverType
752-
}
753-
}
754-
755-
let languageServerTypesCache = LanguageServerTypesCache(index: index.unchecked)
733+
var locationsByFile: [URL: (renameLocations: [RenameLocation], symbolProvider: SymbolProviderKind)] = [:]
756734

757735
let usrsToRename = overridingAndOverriddenUsrs(of: usr, index: index)
758736
let occurrencesToRename = usrsToRename.flatMap { index.occurrences(ofUSR: $0, roles: renameRoles) }
@@ -767,19 +745,16 @@ extension SourceKitLSPServer {
767745
// perform an indexed rename for it.
768746
continue
769747
}
770-
switch await languageServerTypesCache.languageServerType(for: url) {
748+
switch occurrence.symbolProvider {
771749
case .swift:
772750
// sourcekitd only produces AST-based results for the direct calls to this USR. This is because the Swift
773751
// AST only has upwards references to superclasses and overridden methods, not the other way round. It is
774752
// thus not possible to (easily) compute an up-down closure like described in `overridingAndOverriddenUsrs`.
775753
// We thus need to perform an indexed rename for other, related USRs.
776754
break
777-
case .clangd:
755+
case .clang:
778756
// clangd produces AST-based results for the entire class hierarchy, so nothing to do.
779757
continue
780-
case nil:
781-
// Unknown symbol provider
782-
continue
783758
}
784759
}
785760

@@ -788,26 +763,40 @@ extension SourceKitLSPServer {
788763
utf8Column: occurrence.location.utf8Column,
789764
usage: RenameLocation.Usage(roles: occurrence.roles)
790765
)
791-
locationsByFile[url, default: []].append(renameLocation)
766+
if let existingLocations = locationsByFile[url] {
767+
if existingLocations.symbolProvider != occurrence.symbolProvider {
768+
logger.fault(
769+
"""
770+
Found mismatching symbol providers for \(DocumentURI(url).forLogging): \
771+
\(String(describing: existingLocations.symbolProvider), privacy: .public) vs \
772+
\(String(describing: occurrence.symbolProvider), privacy: .public)
773+
"""
774+
)
775+
}
776+
locationsByFile[url] = (existingLocations.renameLocations + [renameLocation], occurrence.symbolProvider)
777+
} else {
778+
locationsByFile[url] = ([renameLocation], occurrence.symbolProvider)
779+
}
792780
}
793781

794782
// Now, call `editsToRename(locations:in:oldName:newName:)` on the language service to convert these ranges into
795783
// edits.
796784
let urisAndEdits =
797785
await locationsByFile
798-
.concurrentMap { (url: URL, renameLocations: [RenameLocation]) -> (DocumentURI, [TextEdit])? in
786+
.concurrentMap {
787+
(
788+
url: URL,
789+
value: (renameLocations: [RenameLocation], symbolProvider: SymbolProviderKind)
790+
) -> (DocumentURI, [TextEdit])? in
799791
let uri = DocumentURI(url)
800792
let language: Language
801-
switch await languageServerTypesCache.languageServerType(for: url) {
802-
case .clangd:
793+
switch value.symbolProvider {
794+
case .clang:
803795
// Technically, we still don't know the language of the source file but defaulting to C is sufficient to
804796
// ensure we get the clang toolchain language server, which is all we care about.
805797
language = .c
806798
case .swift:
807799
language = .swift
808-
case nil:
809-
logger.error("Failed to determine symbol provider for \(uri.forLogging)")
810-
return nil
811800
}
812801
// Create a document snapshot to operate on. If the document is open, load it from the document manager,
813802
// otherwise conjure one from the file on disk. We need the file in memory to perform UTF-8 to UTF-16 column
@@ -823,13 +812,13 @@ extension SourceKitLSPServer {
823812
var edits: [TextEdit] =
824813
await orLog("Getting edits for rename location") {
825814
return try await languageService.editsToRename(
826-
locations: renameLocations,
815+
locations: value.renameLocations,
827816
in: snapshot,
828817
oldName: oldName,
829818
newName: newName
830819
)
831820
} ?? []
832-
for location in renameLocations where location.usage == .definition {
821+
for location in value.renameLocations where location.usage == .definition {
833822
edits += await languageService.editsToRenameParametersInFunctionBody(
834823
snapshot: snapshot,
835824
renameLocation: location,

Tests/SourceKitLSPTests/CrossLanguageRenameTests.swift

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ private let libAlibBPackageManifest = """
2323
)
2424
"""
2525

26+
private let libAlibBCxxInteropPackageManifest = """
27+
let package = Package(
28+
name: "MyLibrary",
29+
targets: [
30+
.target(name: "LibA"),
31+
.target(name: "LibB", dependencies: ["LibA"], swiftSettings: [.interoperabilityMode(.Cxx)]),
32+
]
33+
)
34+
"""
35+
2636
final class CrossLanguageRenameTests: XCTestCase {
2737
func testZeroArgCFunction() async throws {
2838
try await SkipUnless.clangdSupportsIndexBasedRename()
@@ -733,4 +743,140 @@ final class CrossLanguageRenameTests: XCTestCase {
733743
manifest: libAlibBPackageManifest
734744
)
735745
}
746+
747+
func testRenameCxxClassExposedToSwift() async throws {
748+
try await SkipUnless.clangdSupportsIndexBasedRename()
749+
try await assertMultiFileRename(
750+
files: [
751+
"LibA/include/LibA.h": """
752+
struct 1️⃣Foo {};
753+
""",
754+
"LibA/LibA.cpp": "",
755+
"LibB/LibB.swift": """
756+
import LibA
757+
758+
func test(foo: 2️⃣Foo) {}
759+
""",
760+
],
761+
headerFileLanguage: .cpp,
762+
newName: "Bar",
763+
expectedPrepareRenamePlaceholder: "Foo",
764+
expected: [
765+
"LibA/include/LibA.h": """
766+
struct Bar {};
767+
""",
768+
"LibA/LibA.cpp": "",
769+
"LibB/LibB.swift": """
770+
import LibA
771+
772+
func test(foo: Bar) {}
773+
""",
774+
],
775+
manifest: libAlibBCxxInteropPackageManifest
776+
)
777+
}
778+
779+
func testRenameCxxMethodExposedToSwift() async throws {
780+
try await SkipUnless.clangdSupportsIndexBasedRename()
781+
try await assertMultiFileRename(
782+
files: [
783+
"LibA/include/LibA.h": """
784+
struct Foo {
785+
int 1️⃣doStuff() const;
786+
};
787+
""",
788+
"LibA/LibA.cpp": """
789+
#include "LibA.h"
790+
791+
int Foo::2️⃣doStuff() const {
792+
return 42;
793+
}
794+
""",
795+
"LibB/LibB.swift": """
796+
import LibA
797+
798+
func test(foo: Foo) {
799+
foo.3️⃣doStuff()
800+
}
801+
""",
802+
],
803+
headerFileLanguage: .cpp,
804+
newName: "doNewStuff",
805+
expectedPrepareRenamePlaceholder: "doStuff",
806+
expected: [
807+
"LibA/include/LibA.h": """
808+
struct Foo {
809+
int doNewStuff() const;
810+
};
811+
""",
812+
"LibA/LibA.cpp": """
813+
#include "LibA.h"
814+
815+
int Foo::doNewStuff() const {
816+
return 42;
817+
}
818+
""",
819+
"LibB/LibB.swift": """
820+
import LibA
821+
822+
func test(foo: Foo) {
823+
foo.doNewStuff()
824+
}
825+
""",
826+
],
827+
manifest: libAlibBCxxInteropPackageManifest
828+
)
829+
}
830+
831+
func testRenameSwiftMethodExposedToSwift() async throws {
832+
try await SkipUnless.clangdSupportsIndexBasedRename()
833+
try await assertMultiFileRename(
834+
files: [
835+
"LibA/include/LibA.h": """
836+
struct Foo {
837+
int 1️⃣doStuff() const;
838+
};
839+
""",
840+
"LibA/LibA.cpp": """
841+
#include "LibA.h"
842+
843+
int Foo::2️⃣doStuff() const {
844+
return 42;
845+
}
846+
""",
847+
"LibB/LibB.swift": """
848+
import LibA
849+
850+
func test(foo: Foo) {
851+
foo.3️⃣doStuff()
852+
}
853+
""",
854+
],
855+
headerFileLanguage: .cpp,
856+
newName: "doNewStuff",
857+
expectedPrepareRenamePlaceholder: "doStuff",
858+
expected: [
859+
"LibA/include/LibA.h": """
860+
struct Foo {
861+
int doNewStuff() const;
862+
};
863+
""",
864+
"LibA/LibA.cpp": """
865+
#include "LibA.h"
866+
867+
int Foo::doNewStuff() const {
868+
return 42;
869+
}
870+
""",
871+
"LibB/LibB.swift": """
872+
import LibA
873+
874+
func test(foo: Foo) {
875+
foo.doNewStuff()
876+
}
877+
""",
878+
],
879+
manifest: libAlibBCxxInteropPackageManifest
880+
)
881+
}
736882
}

0 commit comments

Comments
 (0)