Skip to content

Commit b6f5fbe

Browse files
committed
Support rename of function parameters from variables that reference them
When renaming a function parameter from inside the function body, we expect that to be a local rename and thus we shouldn’t update the external parameter name. We should thus introduce the new renamed variable name as the parameter’s second name. rdar://123536538
1 parent 884b1f5 commit b6f5fbe

File tree

3 files changed

+65
-8
lines changed

3 files changed

+65
-8
lines changed

Sources/SourceKitLSP/Rename.swift

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,13 +1058,20 @@ extension SwiftLanguageServer {
10581058
// Once we have lexical scope lookup in swift-syntax, this can be a purely syntactic rename.
10591059
// We know that the parameters are variables and thus there can't be overloads that need to be resolved by the
10601060
// type checker.
1061-
let relatedIdentifiersResponse = try await self.relatedIdentifiers(
1061+
let relatedIdentifiers = try await self.relatedIdentifiers(
10621062
at: parameterPosition,
10631063
in: snapshot,
10641064
includeNonEditableBaseNames: false
10651065
)
10661066

1067-
let parameterRenameLocations = relatedIdentifiersResponse.renameLocations(in: snapshot)
1067+
// Exclude the edit that renames the parameter itself. The parameter gets renamed as part of the function
1068+
// declaration.
1069+
let filteredRelatedIdentifiers = RelatedIdentifiersResponse(
1070+
relatedIdentifiers: relatedIdentifiers.relatedIdentifiers.filter { !$0.range.contains(parameterPosition) },
1071+
name: relatedIdentifiers.name
1072+
)
1073+
1074+
let parameterRenameLocations = filteredRelatedIdentifiers.renameLocations(in: snapshot)
10681075

10691076
return try await editsToRename(
10701077
locations: parameterRenameLocations,
@@ -1076,9 +1083,7 @@ extension SwiftLanguageServer {
10761083
guard let parameterRenameEdits else {
10771084
continue
10781085
}
1079-
// Exclude the edit that renames the parameter itself. The parameter gets renamed as part of the function
1080-
// declaration.
1081-
edits += parameterRenameEdits.filter { !$0.range.contains(parameterPosition) }
1086+
edits += parameterRenameEdits
10821087
}
10831088
return edits
10841089
}
@@ -1162,6 +1167,8 @@ extension SwiftLanguageServer {
11621167
)
11631168
}
11641169

1170+
let tree = await syntaxTreeManager.syntaxTree(for: snapshot)
1171+
11651172
let compoundRenameRanges = try await getSyntacticRenameRanges(
11661173
renameLocations: renameLocations,
11671174
oldName: oldNameString,
@@ -1187,6 +1194,19 @@ extension SwiftLanguageServer {
11871194
}
11881195
return compoundRenameRange.pieces.compactMap { (piece) -> TextEdit? in
11891196
if piece.kind == .baseName {
1197+
if let absolutePiecePosition = snapshot.absolutePosition(of: piece.range.lowerBound),
1198+
let firstNameToken = tree.token(at: absolutePiecePosition),
1199+
firstNameToken.keyPathInParent == \FunctionParameterSyntax.firstName,
1200+
let parameterSyntax = firstNameToken.parent(as: FunctionParameterSyntax.self),
1201+
parameterSyntax.secondName == nil, // Should always be true because otherwise decl would be second name
1202+
let firstNameEndPos = snapshot.position(of: firstNameToken.endPositionBeforeTrailingTrivia)
1203+
{
1204+
// We are renaming a function parameter from inside the function body.
1205+
// This should be a local rename and it shouldn't affect all the callers of the function. Introduce the new
1206+
// name as a second name.
1207+
return TextEdit(range: firstNameEndPos..<firstNameEndPos, newText: " " + newName.baseName)
1208+
}
1209+
11901210
return TextEdit(range: piece.range, newText: newName.baseName)
11911211
} else if piece.kind == .keywordBaseName {
11921212
// Keyword base names can't be renamed

Sources/SourceKitLSP/Swift/RelatedIdentifiers.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,11 @@ struct RelatedIdentifiersResponse {
6060
}
6161

6262
extension SwiftLanguageServer {
63-
func relatedIdentifiers(at position: Position, in snapshot: DocumentSnapshot, includeNonEditableBaseNames: Bool)
64-
async throws -> RelatedIdentifiersResponse
65-
{
63+
func relatedIdentifiers(
64+
at position: Position,
65+
in snapshot: DocumentSnapshot,
66+
includeNonEditableBaseNames: Bool
67+
) async throws -> RelatedIdentifiersResponse {
6668
guard let offset = snapshot.utf8Offset(of: position) else {
6769
throw ResponseError.unknown("invalid position \(position)")
6870
}

Tests/SourceKitLSPTests/RenameTests.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,4 +825,39 @@ final class RenameTests: XCTestCase {
825825
]
826826
)
827827
}
828+
829+
func testRenameParameterInsideFunction() async throws {
830+
try await assertSingleFileRename(
831+
"""
832+
func test(myParam: Int) {
833+
print(1️⃣myParam)
834+
}
835+
""",
836+
newName: "other",
837+
expectedPrepareRenamePlaceholder: "myParam",
838+
expected: """
839+
func test(myParam other: Int) {
840+
print(other)
841+
}
842+
"""
843+
)
844+
}
845+
846+
func testRenameParameterSecondNameInsideFunction() async throws {
847+
try await assertSingleFileRename(
848+
"""
849+
func test(myExternalName myParam: Int) {
850+
print(1️⃣myParam)
851+
}
852+
""",
853+
newName: "other",
854+
expectedPrepareRenamePlaceholder: "myParam",
855+
expected: """
856+
func test(myExternalName other: Int) {
857+
print(other)
858+
}
859+
"""
860+
)
861+
}
862+
828863
}

0 commit comments

Comments
 (0)