1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465
|
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
@preconcurrency import IndexStoreDB
import LSPLogging
import LanguageServerProtocol
import SKSupport
import SemanticIndex
import SourceKitD
import SwiftSyntax
// MARK: - Helper types
/// A parsed representation of a name that may be disambiguated by its argument labels.
///
/// ### Examples
/// - `foo(a:b:)`
/// - `foo(_:b:)`
/// - `foo` if no argument labels are specified, eg. for a variable.
fileprivate struct CompoundDeclName {
/// The parameter of a compound decl name, which can either be the parameter's name or `_` to indicate that the
/// parameter is unnamed.
enum Parameter: Equatable {
case named(String)
case wildcard
var stringOrWildcard: String {
switch self {
case .named(let str): return str
case .wildcard: return "_"
}
}
var stringOrEmpty: String {
switch self {
case .named(let str): return str
case .wildcard: return ""
}
}
}
let baseName: String
let parameters: [Parameter]
/// Parse a compound decl name into its base names and parameters.
init(_ compoundDeclName: String) {
guard let openParen = compoundDeclName.firstIndex(of: "(") else {
// We don't have a compound name. Everything is the base name
self.baseName = compoundDeclName
self.parameters = []
return
}
self.baseName = String(compoundDeclName[..<openParen])
let closeParen = compoundDeclName.firstIndex(of: ")") ?? compoundDeclName.endIndex
let parametersText = compoundDeclName[compoundDeclName.index(after: openParen)..<closeParen]
// Split by `:` to get the parameter names. Drop the last element so that we don't have a trailing empty element
// after the last `:`.
let parameterStrings = parametersText.split(separator: ":", omittingEmptySubsequences: false).dropLast()
parameters = parameterStrings.map {
switch $0 {
case "", "_": return .wildcard
default: return .named(String($0))
}
}
}
}
/// The kind of range that a `SyntacticRenamePiece` can be.
fileprivate enum SyntacticRenamePieceKind {
/// The base name of a function or the name of a variable, which can be renamed.
///
/// ### Examples
/// - `foo` in `func foo(a b: Int)`.
/// - `foo` in `let foo = 1`
case baseName
/// The base name of a function-like declaration that cannot be renamed
///
/// ### Examples
/// - `init` in `init(a: Int)`
/// - `subscript` in `subscript(a: Int) -> Int`
case keywordBaseName
/// The internal parameter name (aka. second name) inside a function declaration
///
/// ### Examples
/// - ` b` in `func foo(a b: Int)`
case parameterName
/// Same as `parameterName` but cannot be removed if it is the same as the parameter's first name. This only happens
/// for subscripts where parameters are unnamed by default unless they have both a first and second name.
///
/// ### Examples
/// The second ` a` in `subscript(a a: Int)`
case noncollapsibleParameterName
/// The external argument label of a function parameter
///
/// ### Examples
/// - `a` in `func foo(a b: Int)`
/// - `a` in `func foo(a: Int)`
case declArgumentLabel
/// The argument label inside a call.
///
/// ### Examples
/// - `a` in `foo(a: 1)`
case callArgumentLabel
/// The colon after an argument label inside a call. This is reported so it can be removed if the parameter becomes
/// unnamed.
///
/// ### Examples
/// - `: ` in `foo(a: 1)`
case callArgumentColon
/// An empty range that point to the position before an unnamed argument. This is used to insert the argument label
/// if an unnamed parameter becomes named.
///
/// ### Examples
/// - An empty range before `1` in `foo(1)`, which could expand to `foo(a: 1)`
case callArgumentCombined
/// The argument label in a compound decl name.
///
/// ### Examples
/// - `a` in `foo(a:)`
case selectorArgumentLabel
init?(_ uid: sourcekitd_api_uid_t, values: sourcekitd_api_values) {
switch uid {
case values.renameRangeBase: self = .baseName
case values.renameRangeCallArgColon: self = .callArgumentColon
case values.renameRangeCallArgCombined: self = .callArgumentCombined
case values.renameRangeCallArgLabel: self = .callArgumentLabel
case values.renameRangeDeclArgLabel: self = .declArgumentLabel
case values.renameRangeKeywordBase: self = .keywordBaseName
case values.renameRangeNoncollapsibleParam: self = .noncollapsibleParameterName
case values.renameRangeParam: self = .parameterName
case values.renameRangeSelectorArgLabel: self = .selectorArgumentLabel
default: return nil
}
}
}
/// A single “piece” that is used for renaming a compound function name.
///
/// See `SyntacticRenamePieceKind` for the different rename pieces that exist.
///
/// ### Example
/// `foo(x: 1)` is represented by three pieces
/// - The base name `foo`
/// - The parameter name `x`
/// - The call argument colon `: `.
fileprivate struct SyntacticRenamePiece {
/// The range that represents this piece of the name
let range: Range<Position>
/// The kind of the rename piece.
let kind: SyntacticRenamePieceKind
/// If this piece belongs to a parameter, the index of that parameter (zero-based) or `nil` if this is the base name
/// piece.
let parameterIndex: Int?
/// Create a `SyntacticRenamePiece` from a `sourcekitd` response.
init?(
_ dict: SKDResponseDictionary,
in snapshot: DocumentSnapshot,
keys: sourcekitd_api_keys,
values: sourcekitd_api_values
) {
guard let line: Int = dict[keys.line],
let column: Int = dict[keys.column],
let endLine: Int = dict[keys.endLine],
let endColumn: Int = dict[keys.endColumn],
let kind: sourcekitd_api_uid_t = dict[keys.kind]
else {
return nil
}
let start = snapshot.positionOf(zeroBasedLine: line - 1, utf8Column: column - 1)
let end = snapshot.positionOf(zeroBasedLine: endLine - 1, utf8Column: endColumn - 1)
guard let kind = SyntacticRenamePieceKind(kind, values: values) else {
return nil
}
self.range = start..<end
self.kind = kind
self.parameterIndex = dict[keys.argIndex] as Int?
}
}
/// The context in which the location to be renamed occurred.
fileprivate enum SyntacticRenameNameContext {
/// No syntactic rename ranges for the rename location could be found.
case unmatched
/// A name could be found at a requested rename location but the name did not match the specified old name.
case mismatch
/// The matched ranges are in active source code (ie. source code that is not an inactive `#if` range).
case activeCode
/// The matched ranges are in an inactive `#if` region of the source code.
case inactiveCode
/// The matched ranges occur inside a string literal.
case string
/// The matched ranges occur inside a `#selector` directive.
case selector
/// The matched ranges are within a comment.
case comment
init?(_ uid: sourcekitd_api_uid_t, values: sourcekitd_api_values) {
switch uid {
case values.editActive: self = .activeCode
case values.editComment: self = .comment
case values.editInactive: self = .inactiveCode
case values.editMismatch: self = .mismatch
case values.editSelector: self = .selector
case values.editString: self = .string
case values.editUnknown: self = .unmatched
default: return nil
}
}
}
/// A set of ranges that, combined, represent which edits need to be made to rename a possibly compound name.
///
/// See `SyntacticRenamePiece` for more details.
fileprivate struct SyntacticRenameName {
let pieces: [SyntacticRenamePiece]
let category: SyntacticRenameNameContext
init?(
_ dict: SKDResponseDictionary,
in snapshot: DocumentSnapshot,
keys: sourcekitd_api_keys,
values: sourcekitd_api_values
) {
guard let ranges: SKDResponseArray = dict[keys.ranges] else {
return nil
}
self.pieces = ranges.compactMap { SyntacticRenamePiece($0, in: snapshot, keys: keys, values: values) }
guard let categoryUid: sourcekitd_api_uid_t = dict[keys.category],
let category = SyntacticRenameNameContext(categoryUid, values: values)
else {
return nil
}
self.category = category
}
}
private extension LineTable {
/// Returns the string in the source file that's with the given position range.
///
/// If either the lower or upper bound of `range` do not refer to valid positions with in the snapshot, returns
/// `nil` and logs a fault containing the file and line of the caller (from `callerFile` and `callerLine`).
subscript(range: Range<Position>, callerFile: StaticString = #fileID, callerLine: UInt = #line) -> Substring {
let start = self.stringIndexOf(
line: range.lowerBound.line,
utf16Column: range.lowerBound.utf16index,
callerFile: callerFile,
callerLine: callerLine
)
let end = self.stringIndexOf(
line: range.upperBound.line,
utf16Column: range.upperBound.utf16index,
callerFile: callerFile,
callerLine: callerLine
)
return self.content[start..<end]
}
}
private extension RenameLocation.Usage {
init(roles: SymbolRole) {
if roles.contains(.definition) || roles.contains(.declaration) {
self = .definition
} else if roles.contains(.call) {
self = .call
} else {
self = .reference
}
}
}
private extension IndexSymbolKind {
var isMethod: Bool {
switch self {
case .instanceMethod, .classMethod, .staticMethod:
return true
default: return false
}
}
}
// MARK: - Name translation
extension SwiftLanguageService {
enum NameTranslationError: Error, CustomStringConvertible {
case malformedSwiftToClangTranslateNameResponse(SKDResponseDictionary)
case malformedClangToSwiftTranslateNameResponse(SKDResponseDictionary)
var description: String {
switch self {
case .malformedSwiftToClangTranslateNameResponse(let response):
return """
Malformed response for Swift to Clang name translation
\(response.description)
"""
case .malformedClangToSwiftTranslateNameResponse(let response):
return """
Malformed response for Clang to Swift name translation
\(response.description)
"""
}
}
}
/// Translate a Swift name to the corresponding C/C++/ObjectiveC name.
///
/// This invokes the clang importer to perform the name translation, based on the `position` and `uri` at which the
/// Swift symbol is defined.
///
/// - Parameters:
/// - position: The position at which the Swift name is defined
/// - uri: The URI of the document in which the Swift name is defined
/// - name: The Swift name of the symbol
fileprivate func translateSwiftNameToClang(
at symbolLocation: SymbolLocation,
in uri: DocumentURI,
name: CompoundDeclName
) async throws -> String {
guard let snapshot = documentManager.latestSnapshotOrDisk(uri, language: .swift) else {
throw ResponseError.unknown("Failed to get contents of \(uri.forLogging) to translate Swift name to clang name")
}
let req = sourcekitd.dictionary([
keys.request: sourcekitd.requests.nameTranslation,
keys.sourceFile: snapshot.uri.pseudoPath,
keys.compilerArgs: await self.buildSettings(for: snapshot.uri)?.compilerArgs as [SKDRequestValue]?,
keys.offset: snapshot.utf8Offset(of: snapshot.position(of: symbolLocation)),
keys.nameKind: sourcekitd.values.nameSwift,
keys.baseName: name.baseName,
keys.argNames: sourcekitd.array(name.parameters.map { $0.stringOrWildcard }),
])
let response = try await sendSourcekitdRequest(req, fileContents: snapshot.text)
guard let isZeroArgSelector: Int = response[keys.isZeroArgSelector],
let selectorPieces: SKDResponseArray = response[keys.selectorPieces]
else {
throw NameTranslationError.malformedSwiftToClangTranslateNameResponse(response)
}
return
try selectorPieces
.map { (dict: SKDResponseDictionary) -> String in
guard var name: String = dict[keys.name] else {
throw NameTranslationError.malformedSwiftToClangTranslateNameResponse(response)
}
if isZeroArgSelector == 0 {
// Selector pieces in multi-arg selectors end with ":"
name.append(":")
}
return name
}.joined()
}
/// Translates a C/C++/Objective-C symbol name to Swift.
///
/// This requires the position at which the the symbol is referenced in Swift so sourcekitd can determine the
/// clang declaration that is being renamed and check if that declaration has a `SWIFT_NAME`. If it does, this
/// `SWIFT_NAME` is used as the name translation result instead of invoking the clang importer rename rules.
///
/// - Parameters:
/// - position: A position at which this symbol is referenced from Swift.
/// - snapshot: The snapshot containing the `position` that points to a usage of the clang symbol.
/// - isObjectiveCSelector: Whether the name is an Objective-C selector. Cannot be inferred from the name because
/// a name without `:` can also be a zero-arg Objective-C selector. For such names sourcekitd needs to know
/// whether it is translating a selector to apply the correct renaming rule.
/// - name: The clang symbol name.
/// - Returns:
fileprivate func translateClangNameToSwift(
at symbolLocation: SymbolLocation,
in snapshot: DocumentSnapshot,
isObjectiveCSelector: Bool,
name: String
) async throws -> String {
let req = sourcekitd.dictionary([
keys.request: sourcekitd.requests.nameTranslation,
keys.sourceFile: snapshot.uri.pseudoPath,
keys.compilerArgs: await self.buildSettings(for: snapshot.uri)?.compilerArgs as [SKDRequestValue]?,
keys.offset: snapshot.utf8Offset(of: snapshot.position(of: symbolLocation)),
keys.nameKind: sourcekitd.values.nameObjc,
])
if isObjectiveCSelector {
// Split the name into selector pieces, keeping the ':'.
let selectorPieces = name.split(separator: ":").map { String($0 + ":") }
req.set(keys.selectorPieces, to: sourcekitd.array(selectorPieces))
} else {
req.set(keys.baseName, to: name)
}
let response = try await sendSourcekitdRequest(req, fileContents: snapshot.text)
guard let baseName: String = response[keys.baseName] else {
throw NameTranslationError.malformedClangToSwiftTranslateNameResponse(response)
}
let argNamesArray: SKDResponseArray? = response[keys.argNames]
let argNames = try argNamesArray?.map { (dict: SKDResponseDictionary) -> String in
guard var name: String = dict[keys.name] else {
throw NameTranslationError.malformedClangToSwiftTranslateNameResponse(response)
}
if name.isEmpty {
// Empty argument names are represented by `_` in Swift.
name = "_"
}
return name + ":"
}
var result = baseName
if let argNames, !argNames.isEmpty {
result += "(" + argNames.joined() + ")"
}
return result
}
}
/// A name that has a representation both in Swift and clang-based languages.
///
/// These names might differ. For example, an Objective-C method gets translated by the clang importer to form the Swift
/// name or it could have a `SWIFT_NAME` attribute that defines the method's name in Swift. Similarly, a Swift symbol
/// might specify the name by which it gets exposed to Objective-C using the `@objc` attribute.
public struct CrossLanguageName: Sendable {
/// The name of the symbol in clang languages or `nil` if the symbol is defined in Swift, doesn't have any references
/// from clang languages and thus hasn't been translated.
fileprivate let clangName: String?
/// The name of the symbol in Swift or `nil` if the symbol is defined in clang, doesn't have any references from
/// Swift and thus hasn't been translated.
fileprivate let swiftName: String?
fileprivate var compoundSwiftName: CompoundDeclName? {
if let swiftName {
return CompoundDeclName(swiftName)
}
return nil
}
/// the language that the symbol is defined in.
fileprivate let definitionLanguage: Language
/// The name of the symbol in the language that it is defined in.
var definitionName: String? {
switch definitionLanguage {
case .c, .cpp, .objective_c, .objective_cpp:
return clangName
case .swift:
return swiftName
default:
return nil
}
}
}
// MARK: - SourceKitLSPServer
/// The kinds of symbol occurrence roles that should be renamed.
fileprivate let renameRoles: SymbolRole = [.declaration, .definition, .reference]
extension DocumentManager {
/// Returns the latest open snapshot of `uri` or, if no document with that URI is open, reads the file contents of
/// that file from disk.
fileprivate func latestSnapshotOrDisk(_ uri: DocumentURI, language: Language) -> DocumentSnapshot? {
return (try? self.latestSnapshot(uri)) ?? (try? DocumentSnapshot(withContentsFromDisk: uri, language: language))
}
}
extension SourceKitLSPServer {
/// Returns a `DocumentSnapshot`, a position and the corresponding language service that references
/// `usr` from a Swift file. If `usr` is not referenced from Swift, returns `nil`.
private func getReferenceFromSwift(
usr: String,
index: CheckedIndex,
workspace: Workspace
) async -> (swiftLanguageService: SwiftLanguageService, snapshot: DocumentSnapshot, location: SymbolLocation)? {
var reference: SymbolOccurrence? = nil
index.forEachSymbolOccurrence(byUSR: usr, roles: renameRoles) {
if $0.symbolProvider == .swift {
reference = $0
// We have found a reference from Swift. Stop iteration.
return false
}
return true
}
guard let reference else {
return nil
}
let uri = reference.location.documentUri
guard let snapshot = self.documentManager.latestSnapshotOrDisk(uri, language: .swift) else {
return nil
}
let swiftLanguageService = await self.languageService(for: uri, .swift, in: workspace) as? SwiftLanguageService
guard let swiftLanguageService else {
return nil
}
return (swiftLanguageService, snapshot, reference.location)
}
/// Returns a `CrossLanguageName` for the symbol with the given USR.
///
/// If the symbol is used across clang/Swift languages, the cross-language name will have both a `swiftName` and a
/// `clangName` set. Otherwise it only has the name of the language it's defined in set.
///
/// If `overrideName` is passed, the name of the symbol will be assumed to be `overrideName` in its native language.
/// This is used to create a `CrossLanguageName` for the new name of a renamed symbol.
private func getCrossLanguageName(
forUsr usr: String,
overrideName: String? = nil,
workspace: Workspace,
index: CheckedIndex
) async throws -> CrossLanguageName? {
let definitions = index.occurrences(ofUSR: usr, roles: [.definition])
if definitions.isEmpty {
logger.error("No definitions for \(usr) found")
return nil
}
if definitions.count > 1 {
logger.log("Multiple definitions for \(usr) found")
}
// There might be multiple definitions of the same symbol eg. in different `#if` branches. In this case pick any of
// them because with very high likelihood they all translate to the same clang and Swift name. Sort the entries to
// ensure that we deterministically pick the same entry every time.
for definitionOccurrence in definitions.sorted() {
do {
return try await getCrossLanguageName(
forDefinitionOccurrence: definitionOccurrence,
overrideName: overrideName,
workspace: workspace,
index: index
)
} catch {
// If getting the cross-language name fails for this occurrence, try the next definition, if there are multiple.
logger.log(
"Getting cross-language name for occurrence at \(definitionOccurrence.location) failed. \(error.forLogging)"
)
}
}
return nil
}
// FIXME: (async-workaround): Needed to work around rdar://127977642
private func translateClangNameToSwift(
_ swiftLanguageService: SwiftLanguageService,
at symbolLocation: SymbolLocation,
in snapshot: DocumentSnapshot,
isObjectiveCSelector: Bool,
name: String
) async throws -> String {
return try await swiftLanguageService.translateClangNameToSwift(
at: symbolLocation,
in: snapshot,
isObjectiveCSelector: isObjectiveCSelector,
name: name
)
}
private func getCrossLanguageName(
forDefinitionOccurrence definitionOccurrence: SymbolOccurrence,
overrideName: String? = nil,
workspace: Workspace,
index: CheckedIndex
) async throws -> CrossLanguageName {
let definitionSymbol = definitionOccurrence.symbol
let usr = definitionSymbol.usr
let definitionLanguage: Language =
switch definitionSymbol.language {
case .c: .c
case .cxx: .cpp
case .objc: .objective_c
case .swift: .swift
}
let definitionDocumentUri = definitionOccurrence.location.documentUri
guard
let definitionLanguageService = await self.languageService(
for: definitionDocumentUri,
definitionLanguage,
in: workspace
)
else {
throw ResponseError.unknown("Failed to get language service for the document defining \(usr)")
}
let definitionName = overrideName ?? definitionSymbol.name
switch definitionLanguageService {
case is ClangLanguageService:
let swiftName: String?
if let swiftReference = await getReferenceFromSwift(usr: usr, index: index, workspace: workspace) {
let isObjectiveCSelector = definitionLanguage == .objective_c && definitionSymbol.kind.isMethod
swiftName = try await self.translateClangNameToSwift(
swiftReference.swiftLanguageService,
at: swiftReference.location,
in: swiftReference.snapshot,
isObjectiveCSelector: isObjectiveCSelector,
name: definitionName
)
} else {
logger.debug("Not translating \(definitionSymbol) to Swift because it is not referenced from Swift")
swiftName = nil
}
return CrossLanguageName(clangName: definitionName, swiftName: swiftName, definitionLanguage: definitionLanguage)
case let swiftLanguageService as SwiftLanguageService:
// Continue iteration if the symbol provider is not clang.
// If we terminate early by returning `false` from the closure, `forEachSymbolOccurrence` returns `true`,
// indicating that we have found a reference from clang.
let hasReferenceFromClang = !index.forEachSymbolOccurrence(byUSR: usr, roles: renameRoles) {
return $0.symbolProvider != .clang
}
let clangName: String?
if hasReferenceFromClang {
clangName = try await swiftLanguageService.translateSwiftNameToClang(
at: definitionOccurrence.location,
in: definitionDocumentUri,
name: CompoundDeclName(definitionName)
)
} else {
clangName = nil
}
return CrossLanguageName(clangName: clangName, swiftName: definitionName, definitionLanguage: definitionLanguage)
default:
throw ResponseError.unknown("Cannot rename symbol because it is defined in an unknown language")
}
}
/// Starting from the given USR, compute the transitive closure of all declarations that are overridden or override
/// the symbol, including the USR itself.
///
/// This includes symbols that need to traverse the inheritance hierarchy up and down. For example, it includes all
/// occurrences of `foo` in the following when started from `Inherited.foo`.
///
/// ```swift
/// class Base { func foo() {} }
/// class Inherited: Base { override func foo() {} }
/// class OtherInherited: Base { override func foo() {} }
/// ```
private func overridingAndOverriddenUsrs(of usr: String, index: CheckedIndex) -> [String] {
var workList = [usr]
var usrs: [String] = []
while let usr = workList.popLast() {
usrs.append(usr)
var relatedUsrs = index.occurrences(relatedToUSR: usr, roles: .overrideOf).map(\.symbol.usr)
relatedUsrs += index.occurrences(ofUSR: usr, roles: .overrideOf).flatMap { occurrence in
occurrence.relations.filter { $0.roles.contains(.overrideOf) }.map(\.symbol.usr)
}
for overriddenUsr in relatedUsrs {
if usrs.contains(overriddenUsr) || workList.contains(overriddenUsr) {
// Already handling this USR. Nothing to do.
continue
}
workList.append(overriddenUsr)
}
}
return usrs
}
func rename(_ request: RenameRequest) async throws -> WorkspaceEdit? {
let uri = request.textDocument.uri
let snapshot = try documentManager.latestSnapshot(uri)
guard let workspace = await workspaceForDocument(uri: uri) else {
throw ResponseError.workspaceNotOpen(uri)
}
guard let primaryFileLanguageService = workspace.documentService.value[uri] else {
return nil
}
// Determine the local edits and the USR to rename
let renameResult = try await primaryFileLanguageService.rename(request)
// We only check if the files exist. If a source file has been modified on disk, we will still try to perform a
// rename. Rename will check if the expected old name exists at the location in the index and, if not, ignore that
// location. This way we are still able to rename occurrences in files where eg. only one line has been modified but
// all the line:column locations of occurrences are still up-to-date.
// This should match the check level in prepareRename.
guard let usr = renameResult.usr, let index = workspace.index(checkedFor: .deletedFiles) else {
// We don't have enough information to perform a cross-file rename.
return renameResult.edits
}
let oldName = try await getCrossLanguageName(forUsr: usr, workspace: workspace, index: index)
let newName = try await getCrossLanguageName(
forUsr: usr,
overrideName: request.newName,
workspace: workspace,
index: index
)
guard let oldName, let newName else {
// We failed to get the translated name, so we can't to global rename.
// Do local rename within the current file instead as fallback.
return renameResult.edits
}
var changes: [DocumentURI: [TextEdit]] = [:]
if oldName.definitionLanguage == snapshot.language {
// If this is not a cross-language rename, we can use the local edits returned by
// the language service's rename function.
// If this is cross-language rename, that's not possible because the user would eg.
// enter a new clang name, which needs to be translated to the Swift name before
// changing the current file.
changes = renameResult.edits.changes ?? [:]
}
// If we have a USR + old name, perform an index lookup to find workspace-wide symbols to rename.
// First, group all occurrences of that USR by the files they occur in.
var locationsByFile: [DocumentURI: (renameLocations: [RenameLocation], symbolProvider: SymbolProviderKind)] = [:]
let usrsToRename = overridingAndOverriddenUsrs(of: usr, index: index)
let occurrencesToRename = usrsToRename.flatMap { index.occurrences(ofUSR: $0, roles: renameRoles) }
for occurrence in occurrencesToRename {
let uri = occurrence.location.documentUri
// Determine whether we should add the location produced by the index to those that will be renamed, or if it has
// already been handled by the set provided by the AST.
if changes[uri] != nil {
if occurrence.symbol.usr == usr {
// If the language server's rename function already produced AST-based locations for this symbol, no need to
// perform an indexed rename for it.
continue
}
switch occurrence.symbolProvider {
case .swift:
// sourcekitd only produces AST-based results for the direct calls to this USR. This is because the Swift
// AST only has upwards references to superclasses and overridden methods, not the other way round. It is
// thus not possible to (easily) compute an up-down closure like described in `overridingAndOverriddenUsrs`.
// We thus need to perform an indexed rename for other, related USRs.
break
case .clang:
// clangd produces AST-based results for the entire class hierarchy, so nothing to do.
continue
}
}
let renameLocation = RenameLocation(
line: occurrence.location.line,
utf8Column: occurrence.location.utf8Column,
usage: RenameLocation.Usage(roles: occurrence.roles)
)
if let existingLocations = locationsByFile[uri] {
if existingLocations.symbolProvider != occurrence.symbolProvider {
logger.fault(
"""
Found mismatching symbol providers for \(uri.forLogging): \
\(String(describing: existingLocations.symbolProvider), privacy: .public) vs \
\(String(describing: occurrence.symbolProvider), privacy: .public)
"""
)
}
locationsByFile[uri] = (existingLocations.renameLocations + [renameLocation], occurrence.symbolProvider)
} else {
locationsByFile[uri] = ([renameLocation], occurrence.symbolProvider)
}
}
// Now, call `editsToRename(locations:in:oldName:newName:)` on the language service to convert these ranges into
// edits.
let urisAndEdits =
await locationsByFile
.concurrentMap {
(
uri: DocumentURI,
value: (renameLocations: [RenameLocation], symbolProvider: SymbolProviderKind)
) -> (DocumentURI, [TextEdit])? in
let language: Language
switch value.symbolProvider {
case .clang:
// Technically, we still don't know the language of the source file but defaulting to C is sufficient to
// ensure we get the clang toolchain language server, which is all we care about.
language = .c
case .swift:
language = .swift
}
// Create a document snapshot to operate on. If the document is open, load it from the document manager,
// otherwise conjure one from the file on disk. We need the file in memory to perform UTF-8 to UTF-16 column
// conversions.
guard let snapshot = self.documentManager.latestSnapshotOrDisk(uri, language: language) else {
logger.error("Failed to get document snapshot for \(uri.forLogging)")
return nil
}
guard let languageService = await self.languageService(for: uri, language, in: workspace) else {
return nil
}
var edits: [TextEdit] =
await orLog("Getting edits for rename location") {
return try await languageService.editsToRename(
locations: value.renameLocations,
in: snapshot,
oldName: oldName,
newName: newName
)
} ?? []
for location in value.renameLocations where location.usage == .definition {
edits += await languageService.editsToRenameParametersInFunctionBody(
snapshot: snapshot,
renameLocation: location,
newName: newName
)
}
edits = edits.filter { !$0.isNoOp(in: snapshot) }
return (uri, edits)
}.compactMap { $0 }
for (uri, editsForUri) in urisAndEdits {
if !editsForUri.isEmpty {
changes[uri, default: []] += editsForUri
}
}
var edits = renameResult.edits
edits.changes = changes
return edits
}
func prepareRename(
_ request: PrepareRenameRequest,
workspace: Workspace,
languageService: LanguageService
) async throws -> PrepareRenameResponse? {
guard let languageServicePrepareRename = try await languageService.prepareRename(request) else {
return nil
}
var prepareRenameResult = languageServicePrepareRename.prepareRename
guard
let index = workspace.index(checkedFor: .deletedFiles),
let usr = languageServicePrepareRename.usr,
let oldName = try await self.getCrossLanguageName(forUsr: usr, workspace: workspace, index: index),
var definitionName = oldName.definitionName
else {
return prepareRenameResult
}
if oldName.definitionLanguage == .swift, definitionName.hasSuffix("()") {
definitionName = String(definitionName.dropLast(2))
}
// Get the name of the symbol's definition, if possible.
// This is necessary for cross-language rename. Eg. when renaming an Objective-C method from Swift,
// the user still needs to enter the new Objective-C name.
prepareRenameResult.placeholder = definitionName
return prepareRenameResult
}
func indexedRename(
_ request: IndexedRenameRequest,
workspace: Workspace,
languageService: LanguageService
) async throws -> WorkspaceEdit? {
return try await languageService.indexedRename(request)
}
}
// MARK: - Swift
extension SwiftLanguageService {
/// From a list of rename locations compute the list of `SyntacticRenameName`s that define which ranges need to be
/// edited to rename a compound decl name.
///
/// - Parameters:
/// - renameLocations: The locations to rename
/// - oldName: The compound decl name that the declaration had before the rename. Used to verify that the rename
/// locations match that name. Eg. `myFunc(argLabel:otherLabel:)` or `myVar`
/// - snapshot: A `DocumentSnapshot` containing the contents of the file for which to compute the rename ranges.
private func getSyntacticRenameRanges(
renameLocations: [RenameLocation],
oldName: String,
in snapshot: DocumentSnapshot
) async throws -> [SyntacticRenameName] {
let locations = sourcekitd.array(
renameLocations.map { renameLocation in
let location = sourcekitd.dictionary([
keys.line: renameLocation.line,
keys.column: renameLocation.utf8Column,
keys.nameType: renameLocation.usage.uid(values: values),
])
return sourcekitd.dictionary([
keys.locations: [location],
keys.name: oldName,
])
}
)
let skreq = sourcekitd.dictionary([
keys.request: requests.findRenameRanges,
keys.sourceFile: snapshot.uri.pseudoPath,
// find-syntactic-rename-ranges is a syntactic sourcekitd request that doesn't use the in-memory file snapshot.
// We need to send the source text again.
keys.sourceText: snapshot.text,
keys.renameLocations: locations,
])
let syntacticRenameRangesResponse = try await sendSourcekitdRequest(skreq, fileContents: snapshot.text)
guard let categorizedRanges: SKDResponseArray = syntacticRenameRangesResponse[keys.categorizedRanges] else {
throw ResponseError.internalError("sourcekitd did not return categorized ranges")
}
return categorizedRanges.compactMap { SyntacticRenameName($0, in: snapshot, keys: keys, values: values) }
}
/// If `position` is on an argument label or a parameter name, find the range from the function's base name to the
/// token that terminates the arguments or parameters of the function. Typically, this is the closing ')' but it can
/// also be a closing ']' for subscripts or the end of a trailing closure.
private func findFunctionLikeRange(of position: Position, in snapshot: DocumentSnapshot) async -> Range<Position>? {
let tree = await self.syntaxTreeManager.syntaxTree(for: snapshot)
guard let token = tree.token(at: snapshot.absolutePosition(of: position)) else {
return nil
}
// The node that contains the function's base name. This might be an expression like `self.doStuff`.
// The start position of the last token in this node will be used as the base name position.
var startToken: TokenSyntax? = nil
var endToken: TokenSyntax? = nil
switch token.keyPathInParent {
case \LabeledExprSyntax.label:
let callLike = token.parent(as: LabeledExprSyntax.self)?.parent(as: LabeledExprListSyntax.self)?.parent
switch callLike?.as(SyntaxEnum.self) {
case .attribute(let attribute):
startToken = attribute.attributeName.lastToken(viewMode: .sourceAccurate)
endToken = attribute.lastToken(viewMode: .sourceAccurate)
case .functionCallExpr(let functionCall):
startToken = functionCall.calledExpression.lastToken(viewMode: .sourceAccurate)
endToken = functionCall.lastToken(viewMode: .sourceAccurate)
case .macroExpansionDecl(let macroExpansionDecl):
startToken = macroExpansionDecl.macroName
endToken = macroExpansionDecl.lastToken(viewMode: .sourceAccurate)
case .macroExpansionExpr(let macroExpansionExpr):
startToken = macroExpansionExpr.macroName
endToken = macroExpansionExpr.lastToken(viewMode: .sourceAccurate)
case .subscriptCallExpr(let subscriptCall):
startToken = subscriptCall.leftSquare
endToken = subscriptCall.lastToken(viewMode: .sourceAccurate)
default:
break
}
case \FunctionParameterSyntax.firstName:
let parameterClause =
token
.parent(as: FunctionParameterSyntax.self)?
.parent(as: FunctionParameterListSyntax.self)?
.parent(as: FunctionParameterClauseSyntax.self)
if let functionSignature = parameterClause?.parent(as: FunctionSignatureSyntax.self) {
switch functionSignature.parent?.as(SyntaxEnum.self) {
case .functionDecl(let functionDecl):
startToken = functionDecl.name
endToken = functionSignature.parameterClause.rightParen
case .initializerDecl(let initializerDecl):
startToken = initializerDecl.initKeyword
endToken = functionSignature.parameterClause.rightParen
case .macroDecl(let macroDecl):
startToken = macroDecl.name
endToken = functionSignature.parameterClause.rightParen
default:
break
}
} else if let subscriptDecl = parameterClause?.parent(as: SubscriptDeclSyntax.self) {
startToken = subscriptDecl.subscriptKeyword
endToken = subscriptDecl.parameterClause.rightParen
}
case \DeclNameArgumentSyntax.name:
let declReference =
token
.parent(as: DeclNameArgumentSyntax.self)?
.parent(as: DeclNameArgumentListSyntax.self)?
.parent(as: DeclNameArgumentsSyntax.self)?
.parent(as: DeclReferenceExprSyntax.self)
startToken = declReference?.baseName
endToken = declReference?.argumentNames?.rightParen
default:
break
}
if let startToken, let endToken {
return snapshot.range(
of: startToken.positionAfterSkippingLeadingTrivia..<endToken.endPositionBeforeTrailingTrivia
)
}
return nil
}
/// When the user requested a rename at `position` in `snapshot`, determine the position at which the rename should be
/// performed internally, the USR of the symbol to rename and the range to rename that should be returned to the
/// editor.
///
/// This is necessary to adjust the rename position when renaming function parameters. For example when invoking
/// rename on `x` in `foo(x:)`, we need to perform a rename of `foo` in sourcekitd so that we can rename the function
/// parameter.
///
/// The position might be `nil` if there is no local position in the file that refers to the base name to be renamed.
/// This happens if renaming a function parameter of `MyStruct(x:)` where `MyStruct` is defined outside of the current
/// file. In this case, there is no base name that refers to the initializer of `MyStruct`. When `position` is `nil`
/// a pure index-based rename from the usr USR or `symbolDetails` needs to be performed and no `relatedIdentifiers`
/// request can be used to rename symbols in the current file.
///
/// `position` might be at a different location in the source file than where the user initiated the rename.
/// For example, `position` could point to the definition of a function within the file when rename was initiated on
/// a call.
///
/// If a `functionLikeRange` is returned, this is an expanded range that contains both the symbol to rename as well
/// as the position at which the rename was requested. For example, when rename was initiated from the argument label
/// of a function call, the `range` will contain the entire function call from the base name to the closing `)`.
func symbolToRename(
at position: Position,
in snapshot: DocumentSnapshot
) async -> (position: Position?, usr: String?, functionLikeRange: Range<Position>?) {
let startOfIdentifierPosition = await adjustPositionToStartOfIdentifier(position, in: snapshot)
let symbolInfo = try? await self.symbolInfo(
SymbolInfoRequest(textDocument: TextDocumentIdentifier(snapshot.uri), position: startOfIdentifierPosition)
)
guard let functionLikeRange = await findFunctionLikeRange(of: startOfIdentifierPosition, in: snapshot) else {
return (startOfIdentifierPosition, symbolInfo?.only?.usr, nil)
}
if let onlySymbol = symbolInfo?.only, onlySymbol.kind == .constructor {
// We have a rename like `MyStruct(x: 1)`, invoked from `x`.
if let bestLocalDeclaration = onlySymbol.bestLocalDeclaration, bestLocalDeclaration.uri == snapshot.uri {
// If the initializer is declared within the same file, we can perform rename in the current file based on
// the declaration's location.
return (bestLocalDeclaration.range.lowerBound, onlySymbol.usr, functionLikeRange)
}
// Otherwise, we don't have a reference to the base name of the initializer and we can't use related
// identifiers to perform the rename.
// Return `nil` for the position to perform a pure index-based rename.
return (nil, onlySymbol.usr, functionLikeRange)
}
// Adjust the symbol info to the symbol info of the base name.
// This ensures that we get the symbol info of the function's base instead of the parameter.
let baseNameSymbolInfo = try? await self.symbolInfo(
SymbolInfoRequest(textDocument: TextDocumentIdentifier(snapshot.uri), position: functionLikeRange.lowerBound)
)
return (functionLikeRange.lowerBound, baseNameSymbolInfo?.only?.usr, functionLikeRange)
}
public func rename(_ request: RenameRequest) async throws -> (edits: WorkspaceEdit, usr: String?) {
let snapshot = try self.documentManager.latestSnapshot(request.textDocument.uri)
let (renamePosition, usr, _) = await symbolToRename(at: request.position, in: snapshot)
guard let renamePosition else {
return (edits: WorkspaceEdit(), usr: usr)
}
let relatedIdentifiersResponse = try await self.relatedIdentifiers(
at: renamePosition,
in: snapshot,
includeNonEditableBaseNames: true
)
guard let oldNameString = relatedIdentifiersResponse.name else {
throw ResponseError.unknown("Running sourcekit-lsp with a version of sourcekitd that does not support rename")
}
let renameLocations = relatedIdentifiersResponse.renameLocations(in: snapshot)
try Task.checkCancellation()
let oldName = CrossLanguageName(clangName: nil, swiftName: oldNameString, definitionLanguage: .swift)
let newName = CrossLanguageName(clangName: nil, swiftName: request.newName, definitionLanguage: .swift)
var edits = try await editsToRename(
locations: renameLocations,
in: snapshot,
oldName: oldName,
newName: newName
)
if let compoundSwiftName = oldName.compoundSwiftName, !compoundSwiftName.parameters.isEmpty {
// If we are doing a function rename, run `renameParametersInFunctionBody` for every occurrence of the rename
// location within the current file. If the location is not a function declaration, it will exit early without
// invoking sourcekitd, so it's OK to do this performance-wise.
for renameLocation in renameLocations {
edits += await editsToRenameParametersInFunctionBody(
snapshot: snapshot,
renameLocation: renameLocation,
newName: newName
)
}
}
edits = edits.filter { !$0.isNoOp(in: snapshot) }
if edits.isEmpty {
return (edits: WorkspaceEdit(changes: [:]), usr: usr)
}
return (edits: WorkspaceEdit(changes: [snapshot.uri: edits]), usr: usr)
}
public func editsToRenameParametersInFunctionBody(
snapshot: DocumentSnapshot,
renameLocation: RenameLocation,
newName: CrossLanguageName
) async -> [TextEdit] {
let position = snapshot.absolutePosition(of: renameLocation)
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
let token = syntaxTree.token(at: position)
let parameterClause: FunctionParameterClauseSyntax?
switch token?.keyPathInParent {
case \FunctionDeclSyntax.name:
parameterClause = token?.parent(as: FunctionDeclSyntax.self)?.signature.parameterClause
case \InitializerDeclSyntax.initKeyword:
parameterClause = token?.parent(as: InitializerDeclSyntax.self)?.signature.parameterClause
case \SubscriptDeclSyntax.subscriptKeyword:
parameterClause = token?.parent(as: SubscriptDeclSyntax.self)?.parameterClause
default:
parameterClause = nil
}
guard let parameterClause else {
// We are not at a function-like definition. Nothing to rename.
return []
}
guard let newSwiftNameString = newName.swiftName else {
logger.fault(
"Cannot rename at \(renameLocation.line):\(renameLocation.utf8Column) because new name is not a Swift name"
)
return []
}
let newSwiftName = CompoundDeclName(newSwiftNameString)
var edits: [TextEdit] = []
for (index, parameter) in parameterClause.parameters.enumerated() {
guard parameter.secondName == nil else {
// The parameter has a second name. The function signature only renames the first name and the function body
// refers to the second name. Nothing to do.
continue
}
let oldParameterName = parameter.firstName.text
guard index < newSwiftName.parameters.count else {
// We don't have a new name for this parameter. Nothing to do.
continue
}
let newParameterName = newSwiftName.parameters[index].stringOrEmpty
guard !newParameterName.isEmpty else {
// We are changing the parameter to an empty name. This will retain the current external parameter name as the
// new second name, so nothing to do in the function body.
continue
}
guard newParameterName != oldParameterName else {
// This parameter wasn't modified. Nothing to do.
continue
}
let oldCrossLanguageParameterName = CrossLanguageName(
clangName: nil,
swiftName: oldParameterName,
definitionLanguage: .swift
)
let newCrossLanguageParameterName = CrossLanguageName(
clangName: nil,
swiftName: newParameterName,
definitionLanguage: .swift
)
let parameterRenameEdits = await orLog("Renaming parameter") {
let parameterPosition = snapshot.position(of: parameter.positionAfterSkippingLeadingTrivia)
// Once we have lexical scope lookup in swift-syntax, this can be a purely syntactic rename.
// We know that the parameters are variables and thus there can't be overloads that need to be resolved by the
// type checker.
let relatedIdentifiers = try await self.relatedIdentifiers(
at: parameterPosition,
in: snapshot,
includeNonEditableBaseNames: false
)
// Exclude the edit that renames the parameter itself. The parameter gets renamed as part of the function
// declaration.
let filteredRelatedIdentifiers = RelatedIdentifiersResponse(
relatedIdentifiers: relatedIdentifiers.relatedIdentifiers.filter { !$0.range.contains(parameterPosition) },
name: relatedIdentifiers.name
)
let parameterRenameLocations = filteredRelatedIdentifiers.renameLocations(in: snapshot)
return try await editsToRename(
locations: parameterRenameLocations,
in: snapshot,
oldName: oldCrossLanguageParameterName,
newName: newCrossLanguageParameterName
)
}
guard let parameterRenameEdits else {
continue
}
edits += parameterRenameEdits
}
return edits
}
/// Return the edit that needs to be performed for the given syntactic rename piece to rename it from
/// `oldParameter` to `newParameter`.
/// Returns `nil` if no edit needs to be performed.
private func textEdit(
for piece: SyntacticRenamePiece,
in snapshot: DocumentSnapshot,
oldParameter: CompoundDeclName.Parameter,
newParameter: CompoundDeclName.Parameter
) -> TextEdit? {
switch piece.kind {
case .parameterName:
if newParameter == .wildcard, piece.range.isEmpty, case .named(let oldParameterName) = oldParameter {
// We are changing a named parameter to an unnamed one. If the parameter didn't have an internal parameter
// name, we need to transfer the previously external parameter name to be the internal one.
// E.g. `func foo(a: Int)` becomes `func foo(_ a: Int)`.
return TextEdit(range: piece.range, newText: " " + oldParameterName)
}
if case .named(let newParameterLabel) = newParameter,
newParameterLabel.trimmingCharacters(in: .whitespaces)
== snapshot.lineTable[piece.range].trimmingCharacters(in: .whitespaces)
{
// We are changing the external parameter name to be the same one as the internal parameter name. The
// internal name is thus no longer needed. Drop it.
// Eg. an old declaration `func foo(_ a: Int)` becomes `func foo(a: Int)` when renaming the parameter to `a`
return TextEdit(range: piece.range, newText: "")
}
// In all other cases, don't touch the internal parameter name. It's not part of the public API.
return nil
case .noncollapsibleParameterName:
// Noncollapsible parameter names should never be renamed because they are the same as `parameterName` but
// never fall into one of the two categories above.
return nil
case .declArgumentLabel:
if piece.range.isEmpty {
// If we are inserting a new external argument label where there wasn't one before, add a space after it to
// separate it from the internal name.
// E.g. `subscript(a: Int)` becomes `subscript(a a: Int)`.
return TextEdit(range: piece.range, newText: newParameter.stringOrWildcard + " ")
}
// Otherwise, just update the name.
return TextEdit(range: piece.range, newText: newParameter.stringOrWildcard)
case .callArgumentLabel:
// Argument labels of calls are just updated.
return TextEdit(range: piece.range, newText: newParameter.stringOrEmpty)
case .callArgumentColon:
if case .wildcard = newParameter {
// If the parameter becomes unnamed, remove the colon after the argument name.
return TextEdit(range: piece.range, newText: "")
}
return nil
case .callArgumentCombined:
if case .named(let newParameterName) = newParameter {
// If an unnamed parameter becomes named, insert the new name and a colon.
return TextEdit(range: piece.range, newText: newParameterName + ": ")
}
return nil
case .selectorArgumentLabel:
return TextEdit(range: piece.range, newText: newParameter.stringOrWildcard)
case .baseName, .keywordBaseName:
preconditionFailure("Handled above")
}
}
public func editsToRename(
locations renameLocations: [RenameLocation],
in snapshot: DocumentSnapshot,
oldName oldCrossLanguageName: CrossLanguageName,
newName newCrossLanguageName: CrossLanguageName
) async throws -> [TextEdit] {
guard
let oldNameString = oldCrossLanguageName.swiftName,
let oldName = oldCrossLanguageName.compoundSwiftName,
let newName = newCrossLanguageName.compoundSwiftName
else {
throw ResponseError.unknown(
"Failed to rename \(snapshot.uri.forLogging) because the Swift name for rename is unknown"
)
}
let tree = await syntaxTreeManager.syntaxTree(for: snapshot)
let compoundRenameRanges = try await getSyntacticRenameRanges(
renameLocations: renameLocations,
oldName: oldNameString,
in: snapshot
)
try Task.checkCancellation()
return compoundRenameRanges.flatMap { (compoundRenameRange) -> [TextEdit] in
switch compoundRenameRange.category {
case .unmatched, .mismatch:
// The location didn't match. Don't rename it
return []
case .activeCode, .inactiveCode, .selector:
// Occurrences in active code and selectors should always be renamed.
// Inactive code is currently never returned by sourcekitd.
break
case .string, .comment:
// We currently never get any results in strings or comments because the related identifiers request doesn't
// provide any locations inside strings or comments. We would need to have a textual index to find these
// locations.
return []
}
return compoundRenameRange.pieces.compactMap { (piece) -> TextEdit? in
if piece.kind == .baseName {
if let firstNameToken = tree.token(at: snapshot.absolutePosition(of: piece.range.lowerBound)),
firstNameToken.keyPathInParent == \FunctionParameterSyntax.firstName,
let parameterSyntax = firstNameToken.parent(as: FunctionParameterSyntax.self),
parameterSyntax.secondName == nil // Should always be true because otherwise decl would be second name
{
// We are renaming a function parameter from inside the function body.
// This should be a local rename and it shouldn't affect all the callers of the function. Introduce the new
// name as a second name.
return TextEdit(
range: Range(snapshot.position(of: firstNameToken.endPositionBeforeTrailingTrivia)),
newText: " " + newName.baseName
)
}
return TextEdit(range: piece.range, newText: newName.baseName)
} else if piece.kind == .keywordBaseName {
// Keyword base names can't be renamed
return nil
}
guard let parameterIndex = piece.parameterIndex,
parameterIndex < newName.parameters.count,
parameterIndex < oldName.parameters.count
else {
// Be lenient and just keep the old parameter names if the new name doesn't specify them, eg. if we are
// renaming `func foo(a: Int, b: Int)` and the user specified `bar(x:)` as the new name.
return nil
}
return self.textEdit(
for: piece,
in: snapshot,
oldParameter: oldName.parameters[parameterIndex],
newParameter: newName.parameters[parameterIndex]
)
}
}
}
public func prepareRename(
_ request: PrepareRenameRequest
) async throws -> (prepareRename: PrepareRenameResponse, usr: String?)? {
let snapshot = try self.documentManager.latestSnapshot(request.textDocument.uri)
let (renamePosition, usr, functionLikeRange) = await symbolToRename(at: request.position, in: snapshot)
guard let renamePosition else {
return nil
}
let response = try await self.relatedIdentifiers(
at: renamePosition,
in: snapshot,
includeNonEditableBaseNames: true
)
guard var name = response.name else {
throw ResponseError.unknown("Running sourcekit-lsp with a version of sourcekitd that does not support rename")
}
if name.hasSuffix("()") {
name = String(name.dropLast(2))
}
guard let relatedIdentRange = response.relatedIdentifiers.first(where: { $0.range.contains(renamePosition) })?.range
else {
return nil
}
return (PrepareRenameResponse(range: functionLikeRange ?? relatedIdentRange, placeholder: name), usr)
}
}
// MARK: - Clang
extension ClangLanguageService {
func rename(_ renameRequest: RenameRequest) async throws -> (edits: WorkspaceEdit, usr: String?) {
async let edits = forwardRequestToClangd(renameRequest)
let symbolInfoRequest = SymbolInfoRequest(
textDocument: renameRequest.textDocument,
position: renameRequest.position
)
let symbolDetail = try await forwardRequestToClangd(symbolInfoRequest).only
return (try await edits ?? WorkspaceEdit(), symbolDetail?.usr)
}
func editsToRename(
locations renameLocations: [RenameLocation],
in snapshot: DocumentSnapshot,
oldName oldCrossLanguageName: CrossLanguageName,
newName newCrossLanguageName: CrossLanguageName
) async throws -> [TextEdit] {
let positions = [
snapshot.uri: renameLocations.compactMap { snapshot.position(of: $0) }
]
guard
let oldName = oldCrossLanguageName.clangName,
let newName = newCrossLanguageName.clangName
else {
throw ResponseError.unknown(
"Failed to rename \(snapshot.uri.forLogging) because the clang name for rename is unknown"
)
}
let request = IndexedRenameRequest(
textDocument: TextDocumentIdentifier(snapshot.uri),
oldName: oldName,
newName: newName,
positions: positions
)
do {
let edits = try await forwardRequestToClangd(request)
return edits?.changes?[snapshot.uri] ?? []
} catch {
logger.error("Failed to get indexed rename edits: \(error.forLogging)")
return []
}
}
public func prepareRename(
_ request: PrepareRenameRequest
) async throws -> (prepareRename: PrepareRenameResponse, usr: String?)? {
guard let prepareRename = try await forwardRequestToClangd(request) else {
return nil
}
let symbolInfo = try await forwardRequestToClangd(
SymbolInfoRequest(textDocument: request.textDocument, position: request.position)
)
return (prepareRename, symbolInfo.only?.usr)
}
public func editsToRenameParametersInFunctionBody(
snapshot: DocumentSnapshot,
renameLocation: RenameLocation,
newName: CrossLanguageName
) async -> [TextEdit] {
// When renaming a clang function name, we don't need to rename any references to the arguments.
return []
}
}
fileprivate extension SyntaxProtocol {
/// Returns the parent node and casts it to the specified type.
func parent<S: SyntaxProtocol>(as syntaxType: S.Type) -> S? {
return parent?.as(S.self)
}
}
fileprivate extension RelatedIdentifiersResponse {
func renameLocations(in snapshot: DocumentSnapshot) -> [RenameLocation] {
return self.relatedIdentifiers.map {
(relatedIdentifier) -> RenameLocation in
let position = relatedIdentifier.range.lowerBound
let utf8Column = snapshot.lineTable.utf8ColumnAt(line: position.line, utf16Column: position.utf16index)
return RenameLocation(line: position.line + 1, utf8Column: utf8Column + 1, usage: relatedIdentifier.usage)
}
}
}
|