Skip to content

Commit 5b119b7

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 f203b3a commit 5b119b7

File tree

4 files changed

+175
-47
lines changed

4 files changed

+175
-47
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 & 42 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,32 +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: [DocumentURI: [RenameLocation]] = [:]
734-
735-
actor LanguageServerTypesCache {
736-
let index: UncheckedIndex
737-
var languageServerTypesCache: [DocumentURI: LanguageServerType?] = [:]
738-
739-
init(index: UncheckedIndex) {
740-
self.index = index
741-
}
742-
743-
func languageServerType(for uri: DocumentURI) -> LanguageServerType? {
744-
if let cachedValue = languageServerTypesCache[uri] {
745-
return cachedValue
746-
}
747-
let serverType: LanguageServerType? =
748-
if let fileURL = uri.fileURL {
749-
LanguageServerType(symbolProvider: index.symbolProvider(for: fileURL.path))
750-
} else {
751-
nil
752-
}
753-
languageServerTypesCache[uri] = serverType
754-
return serverType
755-
}
756-
}
757-
758-
let languageServerTypesCache = LanguageServerTypesCache(index: index.unchecked)
733+
var locationsByFile: [DocumentURI: (renameLocations: [RenameLocation], symbolProvider: SymbolProviderKind)] = [:]
759734

760735
let usrsToRename = overridingAndOverriddenUsrs(of: usr, index: index)
761736
let occurrencesToRename = usrsToRename.flatMap { index.occurrences(ofUSR: $0, roles: renameRoles) }
@@ -770,19 +745,16 @@ extension SourceKitLSPServer {
770745
// perform an indexed rename for it.
771746
continue
772747
}
773-
switch await languageServerTypesCache.languageServerType(for: uri) {
748+
switch occurrence.symbolProvider {
774749
case .swift:
775750
// sourcekitd only produces AST-based results for the direct calls to this USR. This is because the Swift
776751
// AST only has upwards references to superclasses and overridden methods, not the other way round. It is
777752
// thus not possible to (easily) compute an up-down closure like described in `overridingAndOverriddenUsrs`.
778753
// We thus need to perform an indexed rename for other, related USRs.
779754
break
780-
case .clangd:
755+
case .clang:
781756
// clangd produces AST-based results for the entire class hierarchy, so nothing to do.
782757
continue
783-
case nil:
784-
// Unknown symbol provider
785-
continue
786758
}
787759
}
788760

@@ -791,25 +763,39 @@ extension SourceKitLSPServer {
791763
utf8Column: occurrence.location.utf8Column,
792764
usage: RenameLocation.Usage(roles: occurrence.roles)
793765
)
794-
locationsByFile[uri, default: []].append(renameLocation)
766+
if let existingLocations = locationsByFile[uri] {
767+
if existingLocations.symbolProvider != occurrence.symbolProvider {
768+
logger.fault(
769+
"""
770+
Found mismatching symbol providers for \(uri.forLogging): \
771+
\(String(describing: existingLocations.symbolProvider), privacy: .public) vs \
772+
\(String(describing: occurrence.symbolProvider), privacy: .public)
773+
"""
774+
)
775+
}
776+
locationsByFile[uri] = (existingLocations.renameLocations + [renameLocation], occurrence.symbolProvider)
777+
} else {
778+
locationsByFile[uri] = ([renameLocation], occurrence.symbolProvider)
779+
}
795780
}
796781

797782
// Now, call `editsToRename(locations:in:oldName:newName:)` on the language service to convert these ranges into
798783
// edits.
799784
let urisAndEdits =
800785
await locationsByFile
801-
.concurrentMap { (uri: DocumentURI, renameLocations: [RenameLocation]) -> (DocumentURI, [TextEdit])? in
786+
.concurrentMap {
787+
(
788+
uri: DocumentURI,
789+
value: (renameLocations: [RenameLocation], symbolProvider: SymbolProviderKind)
790+
) -> (DocumentURI, [TextEdit])? in
802791
let language: Language
803-
switch await languageServerTypesCache.languageServerType(for: uri) {
804-
case .clangd:
792+
switch value.symbolProvider {
793+
case .clang:
805794
// Technically, we still don't know the language of the source file but defaulting to C is sufficient to
806795
// ensure we get the clang toolchain language server, which is all we care about.
807796
language = .c
808797
case .swift:
809798
language = .swift
810-
case nil:
811-
logger.error("Failed to determine symbol provider for \(uri.forLogging)")
812-
return nil
813799
}
814800
// Create a document snapshot to operate on. If the document is open, load it from the document manager,
815801
// otherwise conjure one from the file on disk. We need the file in memory to perform UTF-8 to UTF-16 column
@@ -825,13 +811,13 @@ extension SourceKitLSPServer {
825811
var edits: [TextEdit] =
826812
await orLog("Getting edits for rename location") {
827813
return try await languageService.editsToRename(
828-
locations: renameLocations,
814+
locations: value.renameLocations,
829815
in: snapshot,
830816
oldName: oldName,
831817
newName: newName
832818
)
833819
} ?? []
834-
for location in renameLocations where location.usage == .definition {
820+
for location in value.renameLocations where location.usage == .definition {
835821
edits += await languageService.editsToRenameParametersInFunctionBody(
836822
snapshot: snapshot,
837823
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)