[clang] [clang-tools-extra] [clangd] Add support to rename Objective-C selectors (PR #78872)

Alex Hoppen via cfe-commits cfe-commits at lists.llvm.org
Thu Feb 1 13:54:58 PST 2024


https://github.com/ahoppen updated https://github.com/llvm/llvm-project/pull/78872

>From 26d9d1a5a342df34cde921f7d9921e7b94dadd87 Mon Sep 17 00:00:00 2001
From: Alex Hoppen <ahoppen at apple.com>
Date: Tue, 19 Dec 2023 15:54:40 -0800
Subject: [PATCH 1/5] [Refactoring] Add capabilities to `SymbolName` to
 represent Objective-C selector names

---
 .../Tooling/Refactoring/Rename/SymbolName.h   | 30 +++++++-----
 clang/lib/Tooling/Refactoring/CMakeLists.txt  |  1 +
 .../Refactoring/Rename/RenamingAction.cpp     |  4 +-
 .../Refactoring/Rename/USRLocFinder.cpp       |  4 +-
 clang/lib/Tooling/Refactoring/SymbolName.cpp  | 46 +++++++++++++++++++
 clang/tools/clang-rename/CMakeLists.txt       |  1 +
 6 files changed, 70 insertions(+), 16 deletions(-)
 create mode 100644 clang/lib/Tooling/Refactoring/SymbolName.cpp

diff --git a/clang/include/clang/Tooling/Refactoring/Rename/SymbolName.h b/clang/include/clang/Tooling/Refactoring/Rename/SymbolName.h
index 6c28d40f3679c..077f315f657e7 100644
--- a/clang/include/clang/Tooling/Refactoring/Rename/SymbolName.h
+++ b/clang/include/clang/Tooling/Refactoring/Rename/SymbolName.h
@@ -15,6 +15,9 @@
 #include "llvm/ADT/StringRef.h"
 
 namespace clang {
+
+class LangOptions;
+
 namespace tooling {
 
 /// A name of a symbol.
@@ -27,19 +30,22 @@ namespace tooling {
 /// //       ^~ string 0 ~~~~~         ^~ string 1 ~~~~~
 /// \endcode
 class SymbolName {
+  llvm::SmallVector<std::string, 1> NamePieces;
+
 public:
-  explicit SymbolName(StringRef Name) {
-    // While empty symbol names are valid (Objective-C selectors can have empty
-    // name pieces), occurrences Objective-C selectors are created using an
-    // array of strings instead of just one string.
-    assert(!Name.empty() && "Invalid symbol name!");
-    this->Name.push_back(Name.str());
-  }
-
-  ArrayRef<std::string> getNamePieces() const { return Name; }
-
-private:
-  llvm::SmallVector<std::string, 1> Name;
+  /// Create a new \c SymbolName with the specified pieces.
+  explicit SymbolName(ArrayRef<StringRef> NamePieces);
+
+  /// Creates a \c SymbolName from the given string representation.
+  ///
+  /// For Objective-C symbol names, this splits a selector into multiple pieces
+  /// on `:`. For all other languages the name is used as the symbol name.
+  SymbolName(StringRef Name, bool IsObjectiveCSelector);
+  SymbolName(StringRef Name, const LangOptions &LangOpts);
+
+  ArrayRef<std::string> getNamePieces() const { return NamePieces; }
+
+  void print(raw_ostream &OS) const;
 };
 
 } // end namespace tooling
diff --git a/clang/lib/Tooling/Refactoring/CMakeLists.txt b/clang/lib/Tooling/Refactoring/CMakeLists.txt
index d3077be8810aa..a4a80ce834431 100644
--- a/clang/lib/Tooling/Refactoring/CMakeLists.txt
+++ b/clang/lib/Tooling/Refactoring/CMakeLists.txt
@@ -13,6 +13,7 @@ add_clang_library(clangToolingRefactoring
   Rename/USRFinder.cpp
   Rename/USRFindingAction.cpp
   Rename/USRLocFinder.cpp
+  SymbolName.cpp
 
   LINK_LIBS
   clangAST
diff --git a/clang/lib/Tooling/Refactoring/Rename/RenamingAction.cpp b/clang/lib/Tooling/Refactoring/Rename/RenamingAction.cpp
index 72598601d47d6..4965977d1f7aa 100644
--- a/clang/lib/Tooling/Refactoring/Rename/RenamingAction.cpp
+++ b/clang/lib/Tooling/Refactoring/Rename/RenamingAction.cpp
@@ -82,7 +82,7 @@ RenameOccurrences::createSourceReplacements(RefactoringRuleContext &Context) {
   if (!Occurrences)
     return Occurrences.takeError();
   // FIXME: Verify that the new name is valid.
-  SymbolName Name(NewName);
+  SymbolName Name(NewName, /*IsObjectiveCSelector=*/false);
   return createRenameReplacements(
       *Occurrences, Context.getASTContext().getSourceManager(), Name);
 }
@@ -219,7 +219,7 @@ class RenamingASTConsumer : public ASTConsumer {
     }
     // FIXME: Support multi-piece names.
     // FIXME: better error handling (propagate error out).
-    SymbolName NewNameRef(NewName);
+    SymbolName NewNameRef(NewName, /*IsObjectiveCSelector=*/false);
     Expected<std::vector<AtomicChange>> Change =
         createRenameReplacements(Occurrences, SourceMgr, NewNameRef);
     if (!Change) {
diff --git a/clang/lib/Tooling/Refactoring/Rename/USRLocFinder.cpp b/clang/lib/Tooling/Refactoring/Rename/USRLocFinder.cpp
index c18f9290471fe..43e48f24caa9e 100644
--- a/clang/lib/Tooling/Refactoring/Rename/USRLocFinder.cpp
+++ b/clang/lib/Tooling/Refactoring/Rename/USRLocFinder.cpp
@@ -60,8 +60,8 @@ class USRLocFindingASTVisitor
                                    const ASTContext &Context)
       : RecursiveSymbolVisitor(Context.getSourceManager(),
                                Context.getLangOpts()),
-        USRSet(USRs.begin(), USRs.end()), PrevName(PrevName), Context(Context) {
-  }
+        USRSet(USRs.begin(), USRs.end()),
+        PrevName(PrevName, /*IsObjectiveCSelector=*/false), Context(Context) {}
 
   bool visitSymbolOccurrence(const NamedDecl *ND,
                              ArrayRef<SourceRange> NameRanges) {
diff --git a/clang/lib/Tooling/Refactoring/SymbolName.cpp b/clang/lib/Tooling/Refactoring/SymbolName.cpp
new file mode 100644
index 0000000000000..7fd50b2c4df7b
--- /dev/null
+++ b/clang/lib/Tooling/Refactoring/SymbolName.cpp
@@ -0,0 +1,46 @@
+//===--- SymbolName.cpp - Clang refactoring library -----------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/Tooling/Refactoring/Rename/SymbolName.h"
+#include "clang/Basic/LangOptions.h"
+#include "llvm/Support/raw_ostream.h"
+
+namespace clang {
+namespace tooling {
+
+SymbolName::SymbolName(StringRef Name, const LangOptions &LangOpts)
+    : SymbolName(Name, LangOpts.ObjC) {}
+
+SymbolName::SymbolName(StringRef Name, bool IsObjectiveCSelector) {
+  if (!IsObjectiveCSelector) {
+    NamePieces.push_back(Name.str());
+    return;
+  }
+  // Decompose an Objective-C selector name into multiple strings.
+  do {
+    auto StringAndName = Name.split(':');
+    NamePieces.push_back(StringAndName.first.str());
+    Name = StringAndName.second;
+  } while (!Name.empty());
+}
+
+SymbolName::SymbolName(ArrayRef<StringRef> NamePieces) {
+  for (const auto &Piece : NamePieces)
+    this->NamePieces.push_back(Piece.str());
+}
+
+void SymbolName::print(raw_ostream &OS) const {
+  for (size_t I = 0, E = NamePieces.size(); I != E; ++I) {
+    if (I != 0)
+      OS << ':';
+    OS << NamePieces[I];
+  }
+}
+
+} // end namespace tooling
+} // end namespace clang
diff --git a/clang/tools/clang-rename/CMakeLists.txt b/clang/tools/clang-rename/CMakeLists.txt
index f4c4e520520d9..2f79f6ce4f3ff 100644
--- a/clang/tools/clang-rename/CMakeLists.txt
+++ b/clang/tools/clang-rename/CMakeLists.txt
@@ -16,6 +16,7 @@ clang_target_link_libraries(clang-rename
   clangTooling
   clangToolingCore
   clangToolingRefactoring
+  clangToolingSyntax
   )
 
 install(FILES clang-rename.py

>From 6b2cf9e33c883671b4e2918a26623733d63ce410 Mon Sep 17 00:00:00 2001
From: Alex Hoppen <ahoppen at apple.com>
Date: Tue, 19 Dec 2023 16:26:45 -0800
Subject: [PATCH 2/5] [clangd] Support renaming of Objective-C method selectors

---
 clang-tools-extra/clangd/refactor/Rename.cpp  | 75 ++++++++++-----
 .../clangd/unittests/RenameTests.cpp          | 55 ++++++++++-
 .../Refactoring/Rename/RenamingAction.h       | 21 +++++
 .../Tooling/Refactoring/Rename/SymbolName.h   | 21 +++++
 clang/lib/Tooling/Refactoring/CMakeLists.txt  |  1 +
 .../Refactoring/Rename/RenamingAction.cpp     | 94 +++++++++++++++++++
 clang/lib/Tooling/Refactoring/SymbolName.cpp  | 20 ++++
 clang/tools/clang-refactor/CMakeLists.txt     |  1 +
 clang/tools/libclang/CMakeLists.txt           |  1 +
 9 files changed, 263 insertions(+), 26 deletions(-)

diff --git a/clang-tools-extra/clangd/refactor/Rename.cpp b/clang-tools-extra/clangd/refactor/Rename.cpp
index 11f9e4627af76..258c1a23d0fe2 100644
--- a/clang-tools-extra/clangd/refactor/Rename.cpp
+++ b/clang-tools-extra/clangd/refactor/Rename.cpp
@@ -26,6 +26,8 @@
 #include "clang/Basic/CharInfo.h"
 #include "clang/Basic/LLVM.h"
 #include "clang/Basic/SourceLocation.h"
+#include "clang/Tooling/Refactoring/Rename/RenamingAction.h"
+#include "clang/Tooling/Refactoring/Rename/SymbolName.h"
 #include "clang/Tooling/Syntax/Tokens.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/StringExtras.h"
@@ -40,6 +42,8 @@ namespace clang {
 namespace clangd {
 namespace {
 
+using tooling::SymbolName;
+
 std::optional<std::string> filePath(const SymbolLocation &Loc,
                                     llvm::StringRef HintFilePath) {
   if (!Loc)
@@ -517,25 +521,28 @@ static bool mayBeValidIdentifier(llvm::StringRef Ident) {
 // Check if we can rename the given RenameDecl into NewName.
 // Return details if the rename would produce a conflict.
 std::optional<InvalidName> checkName(const NamedDecl &RenameDecl,
-                                     llvm::StringRef NewName) {
+                                     const SymbolName &NewName) {
   trace::Span Tracer("CheckName");
   static constexpr trace::Metric InvalidNameMetric(
       "rename_name_invalid", trace::Metric::Counter, "invalid_kind");
   auto &ASTCtx = RenameDecl.getASTContext();
   std::optional<InvalidName> Result;
-  if (isKeyword(NewName, ASTCtx.getLangOpts()))
-    Result = InvalidName{InvalidName::Keywords, NewName.str()};
-  else if (!mayBeValidIdentifier(NewName))
-    Result = InvalidName{InvalidName::BadIdentifier, NewName.str()};
-  else {
-    // Name conflict detection.
-    // Function conflicts are subtle (overloading), so ignore them.
-    if (RenameDecl.getKind() != Decl::Function &&
-        RenameDecl.getKind() != Decl::CXXMethod) {
-      if (auto *Conflict = lookupSiblingWithName(ASTCtx, RenameDecl, NewName))
-        Result = InvalidName{
-            InvalidName::Conflict,
-            Conflict->getLocation().printToString(ASTCtx.getSourceManager())};
+  if (auto Identifier = NewName.getSinglePiece()) {
+    if (isKeyword(*Identifier, ASTCtx.getLangOpts()))
+      Result = InvalidName{InvalidName::Keywords, *Identifier};
+    else if (!mayBeValidIdentifier(*Identifier))
+      Result = InvalidName{InvalidName::BadIdentifier, *Identifier};
+    else {
+      // Name conflict detection.
+      // Function conflicts are subtle (overloading), so ignore them.
+      if (RenameDecl.getKind() != Decl::Function &&
+          RenameDecl.getKind() != Decl::CXXMethod) {
+        if (auto *Conflict =
+                lookupSiblingWithName(ASTCtx, RenameDecl, *Identifier))
+          Result = InvalidName{
+              InvalidName::Conflict,
+              Conflict->getLocation().printToString(ASTCtx.getSourceManager())};
+      }
     }
   }
   if (Result)
@@ -546,7 +553,7 @@ std::optional<InvalidName> checkName(const NamedDecl &RenameDecl,
 // AST-based rename, it renames all occurrences in the main file.
 llvm::Expected<tooling::Replacements>
 renameWithinFile(ParsedAST &AST, const NamedDecl &RenameDecl,
-                 llvm::StringRef NewName) {
+                 const SymbolName &OldName, const SymbolName &NewName) {
   trace::Span Tracer("RenameWithinFile");
   const SourceManager &SM = AST.getSourceManager();
 
@@ -569,9 +576,28 @@ renameWithinFile(ParsedAST &AST, const NamedDecl &RenameDecl,
     //   }
     if (!isInsideMainFile(RenameLoc, SM))
       continue;
-    if (auto Err = FilteredChanges.add(tooling::Replacement(
-            SM, CharSourceRange::getTokenRange(RenameLoc), NewName)))
-      return std::move(Err);
+    if (std::optional<std::string> Identifier = NewName.getSinglePiece()) {
+      tooling::Replacement NewReplacement(
+          SM, CharSourceRange::getTokenRange(RenameLoc), *Identifier);
+      if (auto Err = FilteredChanges.add(NewReplacement))
+        return std::move(Err);
+    } else {
+      llvm::Expected<SmallVector<SourceLocation>> PieceLocations =
+          findObjCSymbolSelectorPieces(
+              AST.getTokens().expandedTokens(), SM, RenameLoc, OldName,
+              tooling::ObjCSymbolSelectorKind::Unknown);
+      if (!PieceLocations) {
+        return PieceLocations.takeError();
+      }
+      assert(PieceLocations->size() == NewName.getNamePieces().size());
+      for (auto [Location, NewPiece] :
+           llvm::zip_equal(*PieceLocations, NewName.getNamePieces())) {
+        tooling::Replacement NewReplacement(
+            SM, CharSourceRange::getTokenRange(Location), NewPiece);
+        if (auto Err = FilteredChanges.add(NewReplacement))
+          return std::move(Err);
+      }
+    }
   }
   return FilteredChanges;
 }
@@ -779,12 +805,14 @@ llvm::Expected<RenameResult> rename(const RenameInputs &RInputs) {
   if (DeclsUnderCursor.size() > 1)
     return makeError(ReasonToReject::AmbiguousSymbol);
   const auto &RenameDecl = **DeclsUnderCursor.begin();
-  const auto *ID = RenameDecl.getIdentifier();
-  if (!ID)
+  DeclarationName Name = RenameDecl.getDeclName();
+  if (!Name)
     return makeError(ReasonToReject::UnsupportedSymbol);
-  if (ID->getName() == RInputs.NewName)
+  SymbolName OldSymbolName(Name);
+  SymbolName NewSymbolName(RInputs.NewName, AST.getLangOpts());
+  if (OldSymbolName == NewSymbolName)
     return makeError(ReasonToReject::SameName);
-  auto Invalid = checkName(RenameDecl, RInputs.NewName);
+  auto Invalid = checkName(RenameDecl, NewSymbolName);
   if (Invalid)
     return makeError(std::move(*Invalid));
 
@@ -802,7 +830,8 @@ llvm::Expected<RenameResult> rename(const RenameInputs &RInputs) {
   // To make cross-file rename work for local symbol, we use a hybrid solution:
   //   - run AST-based rename on the main file;
   //   - run index-based rename on other affected files;
-  auto MainFileRenameEdit = renameWithinFile(AST, RenameDecl, RInputs.NewName);
+  auto MainFileRenameEdit =
+      renameWithinFile(AST, RenameDecl, OldSymbolName, NewSymbolName);
   if (!MainFileRenameEdit)
     return MainFileRenameEdit.takeError();
 
diff --git a/clang-tools-extra/clangd/unittests/RenameTests.cpp b/clang-tools-extra/clangd/unittests/RenameTests.cpp
index 9cbf59684fbc1..ded636b0562bc 100644
--- a/clang-tools-extra/clangd/unittests/RenameTests.cpp
+++ b/clang-tools-extra/clangd/unittests/RenameTests.cpp
@@ -876,6 +876,55 @@ TEST(RenameTest, WithinFileRename) {
   }
 }
 
+TEST(RenameTest, ObjCWithinFileRename) {
+  struct TestCase {
+    llvm::StringRef Input;
+    llvm::StringRef NewName;
+    llvm::StringRef Expected;
+  };
+  TestCase Tests[] = {{
+      R"cpp(
+        @interface Foo
+        - (int)performA^ction:(int)action w^ith:(int)value;
+        @end
+        @implementation Foo
+        - (int)performAc^tion:(int)action w^ith:(int)value {
+          [self performAction:action with:value];
+        }
+        @end
+      )cpp",
+      "performNewAction:by:",
+      R"cpp(
+        @interface Foo
+        - (int)performNewAction:(int)action by:(int)value;
+        @end
+        @implementation Foo
+        - (int)performNewAction:(int)action by:(int)value {
+          [self performNewAction:action by:value];
+        }
+        @end
+      )cpp",
+  }};
+  for (TestCase T : Tests) {
+    SCOPED_TRACE(T.Input);
+    Annotations Code(T.Input);
+    auto TU = TestTU::withCode(Code.code());
+    TU.ExtraArgs.push_back("-xobjective-c");
+    auto AST = TU.build();
+    auto Index = TU.index();
+    for (const auto &RenamePos : Code.points()) {
+      auto RenameResult =
+          rename({RenamePos, T.NewName, AST, testPath(TU.Filename),
+                  getVFSFromAST(AST), Index.get()});
+      ASSERT_TRUE(bool(RenameResult)) << RenameResult.takeError();
+      ASSERT_EQ(1u, RenameResult->GlobalChanges.size());
+      EXPECT_EQ(
+          applyEdits(std::move(RenameResult->GlobalChanges)).front().second,
+          T.Expected);
+    }
+  }
+}
+
 TEST(RenameTest, Renameable) {
   struct Case {
     const char *Code;
@@ -926,12 +975,12 @@ TEST(RenameTest, Renameable) {
         void f(X x) {x+^+;})cpp",
           "no symbol", HeaderFile},
 
-      {R"cpp(// disallow rename on non-normal identifiers.
+      {R"cpp(
          @interface Foo {}
-         -(int) fo^o:(int)x; // Token is an identifier, but declaration name isn't a simple identifier.
+         -(int) [[fo^o]]:(int)x;
          @end
        )cpp",
-       "not a supported kind", HeaderFile},
+       nullptr, HeaderFile},
       {R"cpp(
          void foo(int);
          void foo(char);
diff --git a/clang/include/clang/Tooling/Refactoring/Rename/RenamingAction.h b/clang/include/clang/Tooling/Refactoring/Rename/RenamingAction.h
index 43a8d56e4e717..79c2b384b4eb0 100644
--- a/clang/include/clang/Tooling/Refactoring/Rename/RenamingAction.h
+++ b/clang/include/clang/Tooling/Refactoring/Rename/RenamingAction.h
@@ -19,6 +19,7 @@
 #include "clang/Tooling/Refactoring/RefactoringActionRules.h"
 #include "clang/Tooling/Refactoring/RefactoringOptions.h"
 #include "clang/Tooling/Refactoring/Rename/SymbolOccurrences.h"
+#include "clang/Tooling/Syntax/Tokens.h"
 #include "llvm/Support/Error.h"
 
 namespace clang {
@@ -116,6 +117,26 @@ class QualifiedRenamingAction {
   std::map<std::string, tooling::Replacements> &FileToReplaces;
 };
 
+enum class ObjCSymbolSelectorKind {
+  /// The rename location is an Objective-C method call, eg. `[self add: 1]`.
+  MessageSend,
+
+  /// The rename location is an Objective-C method definition, eg.
+  /// ` - (void)add:(int)theValue`
+  MethodDecl,
+
+  /// It is unknown if the renamed location is a method call or declaration.
+  ///
+  /// The selector kind is being used to improve error recovery, passing unknown
+  /// does not lead to correctness issues.
+  Unknown
+};
+
+llvm::Expected<SmallVector<SourceLocation>> findObjCSymbolSelectorPieces(
+    ArrayRef<syntax::Token> Tokens, const SourceManager &SrcMgr,
+    SourceLocation RenameLoc, const SymbolName &OldName,
+    ObjCSymbolSelectorKind Kind);
+
 } // end namespace tooling
 } // end namespace clang
 
diff --git a/clang/include/clang/Tooling/Refactoring/Rename/SymbolName.h b/clang/include/clang/Tooling/Refactoring/Rename/SymbolName.h
index 077f315f657e7..31520ecabb31a 100644
--- a/clang/include/clang/Tooling/Refactoring/Rename/SymbolName.h
+++ b/clang/include/clang/Tooling/Refactoring/Rename/SymbolName.h
@@ -9,6 +9,7 @@
 #ifndef LLVM_CLANG_TOOLING_REFACTORING_RENAME_SYMBOLNAME_H
 #define LLVM_CLANG_TOOLING_REFACTORING_RENAME_SYMBOLNAME_H
 
+#include "clang/AST/DeclarationName.h"
 #include "clang/Basic/LLVM.h"
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/SmallVector.h"
@@ -36,6 +37,8 @@ class SymbolName {
   /// Create a new \c SymbolName with the specified pieces.
   explicit SymbolName(ArrayRef<StringRef> NamePieces);
 
+  explicit SymbolName(const DeclarationName &Name);
+
   /// Creates a \c SymbolName from the given string representation.
   ///
   /// For Objective-C symbol names, this splits a selector into multiple pieces
@@ -45,7 +48,25 @@ class SymbolName {
 
   ArrayRef<std::string> getNamePieces() const { return NamePieces; }
 
+  /// If this symbol consists of a single piece return it, otherwise return
+  /// `None`.
+  ///
+  /// Only symbols in Objective-C can consist of multiple pieces, so this
+  /// function always returns a value for non-Objective-C symbols.
+  std::optional<std::string> getSinglePiece() const;
+
+  /// Returns a human-readable version of this symbol name.
+  ///
+  /// If the symbol consists of multiple pieces (aka. it is an Objective-C
+  /// selector/method name), the pieces are separated by `:`, otherwise just an
+  /// identifier name.
+  std::string getAsString() const;
+
   void print(raw_ostream &OS) const;
+
+  bool operator==(const SymbolName &Other) const {
+    return NamePieces == Other.NamePieces;
+  }
 };
 
 } // end namespace tooling
diff --git a/clang/lib/Tooling/Refactoring/CMakeLists.txt b/clang/lib/Tooling/Refactoring/CMakeLists.txt
index a4a80ce834431..f78f64ea2ef64 100644
--- a/clang/lib/Tooling/Refactoring/CMakeLists.txt
+++ b/clang/lib/Tooling/Refactoring/CMakeLists.txt
@@ -24,6 +24,7 @@ add_clang_library(clangToolingRefactoring
   clangLex
   clangRewrite
   clangToolingCore
+  clangToolingSyntax
 
   DEPENDS
   omp_gen
diff --git a/clang/lib/Tooling/Refactoring/Rename/RenamingAction.cpp b/clang/lib/Tooling/Refactoring/Rename/RenamingAction.cpp
index 4965977d1f7aa..b76cb06bc8aa9 100644
--- a/clang/lib/Tooling/Refactoring/Rename/RenamingAction.cpp
+++ b/clang/lib/Tooling/Refactoring/Rename/RenamingAction.cpp
@@ -275,5 +275,99 @@ std::unique_ptr<ASTConsumer> QualifiedRenamingAction::newASTConsumer() {
   return std::make_unique<USRSymbolRenamer>(NewNames, USRList, FileToReplaces);
 }
 
+static bool isMatchingSelectorName(const syntax::Token &Tok,
+                                   const syntax::Token &Next,
+                                   StringRef NamePiece,
+                                   const SourceManager &SrcMgr) {
+  if (NamePiece.empty())
+    return Tok.kind() == tok::colon;
+  return (Tok.kind() == tok::identifier || Tok.kind() == tok::raw_identifier) &&
+         Next.kind() == tok::colon && Tok.text(SrcMgr) == NamePiece;
+}
+
+llvm::Expected<SmallVector<SourceLocation>>
+findObjCSymbolSelectorPieces(ArrayRef<syntax::Token> AllTokens,
+                             const SourceManager &SM, SourceLocation RenameLoc,
+                             const SymbolName &OldName,
+                             ObjCSymbolSelectorKind Kind) {
+  ArrayRef<syntax::Token> Tokens =
+      AllTokens.drop_while([RenameLoc](syntax::Token Tok) -> bool {
+        return Tok.location() != RenameLoc;
+      });
+  assert(!Tokens.empty() && "no tokens");
+  assert(OldName.getNamePieces()[0].empty() ||
+         Tokens[0].text(SM) == OldName.getNamePieces()[0]);
+  assert(OldName.getNamePieces().size() > 1);
+  SmallVector<SourceLocation> Result;
+
+  Result.push_back(Tokens[0].location());
+
+  // We have to track square brackets, parens and braces as we want to skip the
+  // tokens inside them. This ensures that we don't use identical selector
+  // pieces in inner message sends, blocks, lambdas and @selector expressions.
+  unsigned SquareCount = 0;
+  unsigned ParenCount = 0;
+  unsigned BraceCount = 0;
+
+  // Start looking for the next selector piece.
+  unsigned Last = Tokens.size() - 1;
+  // Skip the ':' or any other token after the first selector piece token.
+  for (unsigned Index = OldName.getNamePieces()[0].empty() ? 1 : 2;
+       Index < Last; ++Index) {
+    const auto &Tok = Tokens[Index];
+
+    bool NoScoping = SquareCount == 0 && BraceCount == 0 && ParenCount == 0;
+    if (NoScoping &&
+        isMatchingSelectorName(Tok, Tokens[Index + 1],
+                               OldName.getNamePieces()[Result.size()], SM)) {
+      if (!OldName.getNamePieces()[Result.size()].empty()) {
+        // Skip the ':' after the name. This ensures that it won't match a
+        // follow-up selector piece with an empty name.
+        ++Index;
+      }
+      Result.push_back(Tok.location());
+      // All the selector pieces have been found.
+      if (Result.size() == OldName.getNamePieces().size())
+        return Result;
+    } else if (Tok.kind() == tok::r_square) {
+      // Stop scanning at the end of the message send.
+      // Also account for spurious ']' in blocks or lambdas.
+      if (Kind == ObjCSymbolSelectorKind::MessageSend && !SquareCount &&
+          !BraceCount)
+        break;
+      if (SquareCount)
+        --SquareCount;
+    } else if (Tok.kind() == tok::l_square)
+      ++SquareCount;
+    else if (Tok.kind() == tok::l_paren)
+      ++ParenCount;
+    else if (Tok.kind() == tok::r_paren) {
+      if (!ParenCount)
+        break;
+      --ParenCount;
+    } else if (Tok.kind() == tok::l_brace) {
+      // Stop scanning at the start of the of the method's body.
+      // Also account for any spurious blocks inside argument parameter types
+      // or parameter attributes.
+      if (Kind == ObjCSymbolSelectorKind::MethodDecl && !BraceCount &&
+          !ParenCount)
+        break;
+      ++BraceCount;
+    } else if (Tok.kind() == tok::r_brace) {
+      if (!BraceCount)
+        break;
+      --BraceCount;
+    }
+    // Stop scanning at the end of the method's declaration.
+    if (Kind == ObjCSymbolSelectorKind::MethodDecl && NoScoping &&
+        (Tok.kind() == tok::semi || Tok.kind() == tok::minus ||
+         Tok.kind() == tok::plus))
+      break;
+  }
+  return llvm::make_error<llvm::StringError>(
+      "failed to find all selector pieces in the source code",
+      inconvertibleErrorCode());
+}
+
 } // end namespace tooling
 } // end namespace clang
diff --git a/clang/lib/Tooling/Refactoring/SymbolName.cpp b/clang/lib/Tooling/Refactoring/SymbolName.cpp
index 7fd50b2c4df7b..be0dd721b1d22 100644
--- a/clang/lib/Tooling/Refactoring/SymbolName.cpp
+++ b/clang/lib/Tooling/Refactoring/SymbolName.cpp
@@ -13,6 +13,11 @@
 namespace clang {
 namespace tooling {
 
+SymbolName::SymbolName(const DeclarationName &DeclName)
+    : SymbolName(DeclName.getAsString(),
+                 /*IsObjectiveCSelector=*/DeclName.getNameKind() ==
+                     DeclarationName::NameKind::ObjCMultiArgSelector) {}
+
 SymbolName::SymbolName(StringRef Name, const LangOptions &LangOpts)
     : SymbolName(Name, LangOpts.ObjC) {}
 
@@ -34,6 +39,21 @@ SymbolName::SymbolName(ArrayRef<StringRef> NamePieces) {
     this->NamePieces.push_back(Piece.str());
 }
 
+std::optional<std::string> SymbolName::getSinglePiece() const {
+  if (getNamePieces().size() == 1) {
+    return NamePieces.front();
+  } else {
+    return std::nullopt;
+  }
+}
+
+std::string SymbolName::getAsString() const {
+  std::string Result;
+  llvm::raw_string_ostream OS(Result);
+  this->print(OS);
+  return Result;
+}
+
 void SymbolName::print(raw_ostream &OS) const {
   for (size_t I = 0, E = NamePieces.size(); I != E; ++I) {
     if (I != 0)
diff --git a/clang/tools/clang-refactor/CMakeLists.txt b/clang/tools/clang-refactor/CMakeLists.txt
index a21d84d5385b4..092740be15f01 100644
--- a/clang/tools/clang-refactor/CMakeLists.txt
+++ b/clang/tools/clang-refactor/CMakeLists.txt
@@ -20,4 +20,5 @@ clang_target_link_libraries(clang-refactor
   clangTooling
   clangToolingCore
   clangToolingRefactoring
+  clangToolingSyntax
   )
diff --git a/clang/tools/libclang/CMakeLists.txt b/clang/tools/libclang/CMakeLists.txt
index 1cfc46eb1a52f..f400986be797a 100644
--- a/clang/tools/libclang/CMakeLists.txt
+++ b/clang/tools/libclang/CMakeLists.txt
@@ -69,6 +69,7 @@ set(LIBS
   clangSema
   clangSerialization
   clangTooling
+  clangToolingSyntax
 )
 
 if (CLANG_ENABLE_ARCMT)

>From 01234dd67a6bd64df44fea0369335d37482c1a67 Mon Sep 17 00:00:00 2001
From: Alex Hoppen <ahoppen at apple.com>
Date: Wed, 20 Dec 2023 10:57:49 -0800
Subject: [PATCH 3/5] [clangd] Support renaming of Objective-C selectors across
 files

---
 clang-tools-extra/clangd/SourceCode.cpp       |  18 +-
 clang-tools-extra/clangd/SourceCode.h         |   6 +-
 .../clangd/index/SymbolCollector.cpp          |  27 +-
 clang-tools-extra/clangd/refactor/Rename.cpp  |  68 +++--
 clang-tools-extra/clangd/refactor/Rename.h    |  21 +-
 .../clangd/unittests/RenameTests.cpp          | 284 +++++++++++++++++-
 .../clangd/unittests/SourceCodeTests.cpp      |   4 +-
 clang/include/clang/Tooling/Syntax/Tokens.h   |  14 +
 clang/lib/Tooling/Refactoring/SymbolName.cpp  |   4 +-
 clang/lib/Tooling/Syntax/Tokens.cpp           |  11 +
 10 files changed, 395 insertions(+), 62 deletions(-)

diff --git a/clang-tools-extra/clangd/SourceCode.cpp b/clang-tools-extra/clangd/SourceCode.cpp
index 835038423fdf3..0081a69357b02 100644
--- a/clang-tools-extra/clangd/SourceCode.cpp
+++ b/clang-tools-extra/clangd/SourceCode.cpp
@@ -625,16 +625,16 @@ llvm::StringMap<unsigned> collectIdentifiers(llvm::StringRef Content,
   return Identifiers;
 }
 
-std::vector<Range> collectIdentifierRanges(llvm::StringRef Identifier,
-                                           llvm::StringRef Content,
-                                           const LangOptions &LangOpts) {
+std::vector<Range>
+collectIdentifierRanges(llvm::StringRef Identifier,
+                        const syntax::UnexpandedTokenBuffer &Tokens) {
   std::vector<Range> Ranges;
-  lex(Content, LangOpts,
-      [&](const syntax::Token &Tok, const SourceManager &SM) {
-        if (Tok.kind() != tok::identifier || Tok.text(SM) != Identifier)
-          return;
-        Ranges.push_back(halfOpenToRange(SM, Tok.range(SM).toCharRange(SM)));
-      });
+  const SourceManager &SM = Tokens.sourceManager();
+  for (const syntax::Token &Tok : Tokens.tokens()) {
+    if (Tok.kind() != tok::identifier || Tok.text(SM) != Identifier)
+      continue;
+    Ranges.push_back(halfOpenToRange(SM, Tok.range(SM).toCharRange(SM)));
+  }
   return Ranges;
 }
 
diff --git a/clang-tools-extra/clangd/SourceCode.h b/clang-tools-extra/clangd/SourceCode.h
index a1bb44c176120..f60683fc4f21c 100644
--- a/clang-tools-extra/clangd/SourceCode.h
+++ b/clang-tools-extra/clangd/SourceCode.h
@@ -217,9 +217,9 @@ llvm::StringMap<unsigned> collectIdentifiers(llvm::StringRef Content,
                                              const format::FormatStyle &Style);
 
 /// Collects all ranges of the given identifier in the source code.
-std::vector<Range> collectIdentifierRanges(llvm::StringRef Identifier,
-                                           llvm::StringRef Content,
-                                           const LangOptions &LangOpts);
+std::vector<Range>
+collectIdentifierRanges(llvm::StringRef Identifier,
+                        const syntax::UnexpandedTokenBuffer &Tokens);
 
 /// Collects words from the source code.
 /// Unlike collectIdentifiers:
diff --git a/clang-tools-extra/clangd/index/SymbolCollector.cpp b/clang-tools-extra/clangd/index/SymbolCollector.cpp
index bf838e53f2a21..24bd3c426b3e8 100644
--- a/clang-tools-extra/clangd/index/SymbolCollector.cpp
+++ b/clang-tools-extra/clangd/index/SymbolCollector.cpp
@@ -173,9 +173,20 @@ std::optional<RelationKind> indexableRelation(const index::SymbolRelation &R) {
 bool isSpelled(SourceLocation Loc, const NamedDecl &ND) {
   auto Name = ND.getDeclName();
   const auto NameKind = Name.getNameKind();
-  if (NameKind != DeclarationName::Identifier &&
-      NameKind != DeclarationName::CXXConstructorName)
+  bool PrefixComparison;
+  switch (NameKind) {
+  case DeclarationName::Identifier:
+  case DeclarationName::CXXConstructorName:
+  case DeclarationName::ObjCZeroArgSelector:
+    PrefixComparison = false;
+    break;
+  case DeclarationName::ObjCOneArgSelector:
+  case DeclarationName::ObjCMultiArgSelector:
+    PrefixComparison = true;
+    break;
+  default:
     return false;
+  }
   const auto &AST = ND.getASTContext();
   const auto &SM = AST.getSourceManager();
   const auto &LO = AST.getLangOpts();
@@ -183,7 +194,17 @@ bool isSpelled(SourceLocation Loc, const NamedDecl &ND) {
   if (clang::Lexer::getRawToken(Loc, Tok, SM, LO))
     return false;
   auto StrName = Name.getAsString();
-  return clang::Lexer::getSpelling(Tok, SM, LO) == StrName;
+  std::string LexerSpelling = clang::Lexer::getSpelling(Tok, SM, LO);
+  if (PrefixComparison) {
+    // The lexer spelling at the source location is only the first label of an
+    // Objective-C selector, eg. if `StrName` is `performAction:with:`, then the
+    // token at the requested location is `performAction`. Re-building the
+    // entire selector from the lexer is too complicated here, so just perform
+    // a prefix comparison.
+    return StringRef(StrName).starts_with(LexerSpelling);
+  } else {
+    return StrName == LexerSpelling;
+  }
 }
 } // namespace
 
diff --git a/clang-tools-extra/clangd/refactor/Rename.cpp b/clang-tools-extra/clangd/refactor/Rename.cpp
index 258c1a23d0fe2..c6af9c137751f 100644
--- a/clang-tools-extra/clangd/refactor/Rename.cpp
+++ b/clang-tools-extra/clangd/refactor/Rename.cpp
@@ -690,8 +690,9 @@ findOccurrencesOutsideFile(const NamedDecl &RenameDecl,
 // there is no dirty buffer.
 llvm::Expected<FileEdits>
 renameOutsideFile(const NamedDecl &RenameDecl, llvm::StringRef MainFilePath,
-                  llvm::StringRef NewName, const SymbolIndex &Index,
-                  size_t MaxLimitFiles, llvm::vfs::FileSystem &FS) {
+                  SymbolName OldName, SymbolName NewName,
+                  const SymbolIndex &Index, size_t MaxLimitFiles,
+                  llvm::vfs::FileSystem &FS) {
   trace::Span Tracer("RenameOutsideFile");
   auto AffectedFiles = findOccurrencesOutsideFile(RenameDecl, MainFilePath,
                                                   Index, MaxLimitFiles);
@@ -709,10 +710,11 @@ renameOutsideFile(const NamedDecl &RenameDecl, llvm::StringRef MainFilePath,
     }
 
     auto AffectedFileCode = (*ExpBuffer)->getBuffer();
-    auto RenameRanges =
-        adjustRenameRanges(AffectedFileCode, RenameDecl.getNameAsString(),
-                           std::move(FileAndOccurrences.second),
-                           RenameDecl.getASTContext().getLangOpts());
+    syntax::UnexpandedTokenBuffer Tokens(
+        AffectedFileCode, RenameDecl.getASTContext().getLangOpts());
+    std::optional<std::vector<Range>> RenameRanges =
+        adjustRenameRanges(Tokens, OldName.getNamePieces().front(),
+                           std::move(FileAndOccurrences.second));
     if (!RenameRanges) {
       // Our heuristics fails to adjust rename ranges to the current state of
       // the file, it is most likely the index is stale, so we give up the
@@ -721,8 +723,8 @@ renameOutsideFile(const NamedDecl &RenameDecl, llvm::StringRef MainFilePath,
                    "(the index may be stale)",
                    FilePath);
     }
-    auto RenameEdit =
-        buildRenameEdit(FilePath, AffectedFileCode, *RenameRanges, NewName);
+    auto RenameEdit = buildRenameEdit(FilePath, AffectedFileCode, *RenameRanges,
+                                      OldName, NewName, Tokens);
     if (!RenameEdit)
       return error("failed to rename in file {0}: {1}", FilePath,
                    RenameEdit.takeError());
@@ -876,7 +878,8 @@ llvm::Expected<RenameResult> rename(const RenameInputs &RInputs) {
   }
 
   auto OtherFilesEdits = renameOutsideFile(
-      RenameDecl, RInputs.MainFilePath, RInputs.NewName, *RInputs.Index,
+      RenameDecl, RInputs.MainFilePath, OldSymbolName, NewSymbolName,
+      *RInputs.Index,
       Opts.LimitFiles == 0 ? std::numeric_limits<size_t>::max()
                            : Opts.LimitFiles,
       *RInputs.FS);
@@ -889,10 +892,11 @@ llvm::Expected<RenameResult> rename(const RenameInputs &RInputs) {
   return Result;
 }
 
-llvm::Expected<Edit> buildRenameEdit(llvm::StringRef AbsFilePath,
-                                     llvm::StringRef InitialCode,
-                                     std::vector<Range> Occurrences,
-                                     llvm::StringRef NewName) {
+llvm::Expected<Edit>
+buildRenameEdit(llvm::StringRef AbsFilePath, llvm::StringRef InitialCode,
+                std::vector<Range> Occurrences, SymbolName OldName,
+                SymbolName NewName,
+                const syntax::UnexpandedTokenBuffer &Tokens) {
   trace::Span Tracer("BuildRenameEdit");
   SPAN_ATTACH(Tracer, "file_path", AbsFilePath);
   SPAN_ATTACH(Tracer, "rename_occurrences",
@@ -933,12 +937,34 @@ llvm::Expected<Edit> buildRenameEdit(llvm::StringRef AbsFilePath,
     OccurrencesOffsets.push_back({*StartOffset, *EndOffset});
   }
 
+  const SourceManager &SM = Tokens.sourceManager();
+
   tooling::Replacements RenameEdit;
   for (const auto &R : OccurrencesOffsets) {
-    auto ByteLength = R.second - R.first;
-    if (auto Err = RenameEdit.add(
-            tooling::Replacement(AbsFilePath, R.first, ByteLength, NewName)))
-      return std::move(Err);
+    if (std::optional<std::string> Identifier = NewName.getSinglePiece()) {
+      auto ByteLength = R.second - R.first;
+      if (auto Err = RenameEdit.add(tooling::Replacement(
+              AbsFilePath, R.first, ByteLength, *Identifier)))
+        return std::move(Err);
+    } else {
+      llvm::Expected<SmallVector<SourceLocation>> PieceLocations =
+          findObjCSymbolSelectorPieces(
+              Tokens.tokens(), SM,
+              SM.getLocForStartOfFile(SM.getMainFileID())
+                  .getLocWithOffset(R.first),
+              OldName, tooling::ObjCSymbolSelectorKind::Unknown);
+      if (!PieceLocations) {
+        return PieceLocations.takeError();
+      }
+      assert(PieceLocations->size() == NewName.getNamePieces().size());
+      for (auto [Location, NewPiece] :
+           llvm::zip_equal(*PieceLocations, NewName.getNamePieces())) {
+        tooling::Replacement NewReplacement(
+            SM, CharSourceRange::getTokenRange(Location), NewPiece);
+        if (auto Err = RenameEdit.add(NewReplacement))
+          return std::move(Err);
+      }
+    }
   }
   return Edit(InitialCode, std::move(RenameEdit));
 }
@@ -957,13 +983,13 @@ llvm::Expected<Edit> buildRenameEdit(llvm::StringRef AbsFilePath,
 //          were inserted). If such a "near miss" is found, the rename is still
 //          possible
 std::optional<std::vector<Range>>
-adjustRenameRanges(llvm::StringRef DraftCode, llvm::StringRef Identifier,
-                   std::vector<Range> Indexed, const LangOptions &LangOpts) {
+adjustRenameRanges(const syntax::UnexpandedTokenBuffer &Tokens,
+                   llvm::StringRef Identifier, std::vector<Range> Indexed) {
   trace::Span Tracer("AdjustRenameRanges");
   assert(!Indexed.empty());
   assert(llvm::is_sorted(Indexed));
-  std::vector<Range> Lexed =
-      collectIdentifierRanges(Identifier, DraftCode, LangOpts);
+
+  std::vector<Range> Lexed = collectIdentifierRanges(Identifier, Tokens);
   llvm::sort(Lexed);
   return getMappedRanges(Indexed, Lexed);
 }
diff --git a/clang-tools-extra/clangd/refactor/Rename.h b/clang-tools-extra/clangd/refactor/Rename.h
index 91728ba59e5d8..f14f20a6e1759 100644
--- a/clang-tools-extra/clangd/refactor/Rename.h
+++ b/clang-tools-extra/clangd/refactor/Rename.h
@@ -12,6 +12,7 @@
 #include "Protocol.h"
 #include "SourceCode.h"
 #include "clang/Basic/LangOptions.h"
+#include "clang/Tooling/Refactoring/Rename/SymbolName.h"
 #include "llvm/Support/Error.h"
 #include <optional>
 
@@ -66,13 +67,19 @@ struct RenameResult {
 llvm::Expected<RenameResult> rename(const RenameInputs &RInputs);
 
 /// Generates rename edits that replaces all given occurrences with the
-/// NewName.
+/// `NewName`.
+///
+/// `OldName` is and `Tokens` are used to to find the argument labels of
+/// Objective-C selectors.
+///
 /// Exposed for testing only.
+///
 /// REQUIRED: Occurrences is sorted and doesn't have duplicated ranges.
-llvm::Expected<Edit> buildRenameEdit(llvm::StringRef AbsFilePath,
-                                     llvm::StringRef InitialCode,
-                                     std::vector<Range> Occurrences,
-                                     llvm::StringRef NewName);
+llvm::Expected<Edit>
+buildRenameEdit(llvm::StringRef AbsFilePath, llvm::StringRef InitialCode,
+                std::vector<Range> Occurrences, tooling::SymbolName OldName,
+                tooling::SymbolName NewName,
+                const syntax::UnexpandedTokenBuffer &Tokens);
 
 /// Adjusts indexed occurrences to match the current state of the file.
 ///
@@ -85,8 +92,8 @@ llvm::Expected<Edit> buildRenameEdit(llvm::StringRef AbsFilePath,
 /// occurrence has the same length).
 /// REQUIRED: Indexed is sorted.
 std::optional<std::vector<Range>>
-adjustRenameRanges(llvm::StringRef DraftCode, llvm::StringRef Identifier,
-                   std::vector<Range> Indexed, const LangOptions &LangOpts);
+adjustRenameRanges(const syntax::UnexpandedTokenBuffer &Tokens,
+                   llvm::StringRef Identifier, std::vector<Range> Indexed);
 
 /// Calculates the lexed occurrences that the given indexed occurrences map to.
 /// Returns std::nullopt if we don't find a mapping.
diff --git a/clang-tools-extra/clangd/unittests/RenameTests.cpp b/clang-tools-extra/clangd/unittests/RenameTests.cpp
index ded636b0562bc..008639d573da9 100644
--- a/clang-tools-extra/clangd/unittests/RenameTests.cpp
+++ b/clang-tools-extra/clangd/unittests/RenameTests.cpp
@@ -878,33 +878,130 @@ TEST(RenameTest, WithinFileRename) {
 
 TEST(RenameTest, ObjCWithinFileRename) {
   struct TestCase {
+    /// Annotated source code that should be renamed. Every point (indicated by
+    /// `^`) will be used as a rename location.
     llvm::StringRef Input;
+    /// The new name that should be given to the rename locaitons.
     llvm::StringRef NewName;
-    llvm::StringRef Expected;
+    /// The expected rename source code or `nullopt` if we expect rename to
+    /// fail.
+    std::optional<llvm::StringRef> Expected;
   };
-  TestCase Tests[] = {{
+  TestCase Tests[] = {
+    // Simple rename
+    {
+      // Input
       R"cpp(
         @interface Foo
         - (int)performA^ction:(int)action w^ith:(int)value;
         @end
         @implementation Foo
         - (int)performAc^tion:(int)action w^ith:(int)value {
-          [self performAction:action with:value];
+          return [self performAction:action with:value];
         }
         @end
       )cpp",
+      // New name
       "performNewAction:by:",
+      // Expected
       R"cpp(
         @interface Foo
         - (int)performNewAction:(int)action by:(int)value;
         @end
         @implementation Foo
         - (int)performNewAction:(int)action by:(int)value {
-          [self performNewAction:action by:value];
+          return [self performNewAction:action by:value];
+        }
+        @end
+      )cpp",
+    },
+    // Rename selector with macro
+    {
+      // Input
+      R"cpp(
+        #define mySelector - (int)performAction:(int)action with:(int)value
+        @interface Foo
+        ^mySelector;
+        @end
+        @implementation Foo
+        mySelector {
+          return [self performAction:action with:value];
+        }
+        @end
+      )cpp",
+      // New name
+      "performNewAction:by:",
+      // Expected error
+      std::nullopt,
+    },
+    // Rename selector in macro definition
+    {
+      // Input
+      R"cpp(
+        #define mySelector - (int)perform^Action:(int)action with:(int)value
+        @interface Foo
+        mySelector;
+        @end
+        @implementation Foo
+        mySelector {
+          return [self performAction:action with:value];
+        }
+        @end
+      )cpp",
+      // New name
+      "performNewAction:by:",
+      // Expected error
+      std::nullopt,
+    },
+    // Don't rename `@selector`
+    // `@selector` is not tied to a single selector. Eg. there might be multiple
+    // classes in the codebase that implement that selector. It's thus more like
+    // a string literal and we shouldn't rename it.
+    {
+      // Input
+      R"cpp(
+        @interface Foo
+        - (void)performA^ction:(int)action with:(int)value;
+        @end
+        @implementation Foo
+        - (void)performAction:(int)action with:(int)value {
+          SEL mySelector = @selector(performAction:with:);
+        }
+        @end
+      )cpp",
+      // New name
+      "performNewAction:by:",
+      // Expected
+      R"cpp(
+        @interface Foo
+        - (void)performNewAction:(int)action by:(int)value;
+        @end
+        @implementation Foo
+        - (void)performNewAction:(int)action by:(int)value {
+          SEL mySelector = @selector(performAction:with:);
         }
         @end
       )cpp",
-  }};
+    },
+    // Fail if rename initiated inside @selector
+    {
+      // Input
+      R"cpp(
+        @interface Foo
+        - (void)performAction:(int)action with:(int)value;
+        @end
+        @implementation Foo
+        - (void)performAction:(int)action with:(int)value {
+          SEL mySelector = @selector(perfo^rmAction:with:);
+        }
+        @end
+      )cpp",
+      // New name
+      "performNewAction:by:",
+      // Expected
+      std::nullopt,
+    }
+  };
   for (TestCase T : Tests) {
     SCOPED_TRACE(T.Input);
     Annotations Code(T.Input);
@@ -916,11 +1013,16 @@ TEST(RenameTest, ObjCWithinFileRename) {
       auto RenameResult =
           rename({RenamePos, T.NewName, AST, testPath(TU.Filename),
                   getVFSFromAST(AST), Index.get()});
-      ASSERT_TRUE(bool(RenameResult)) << RenameResult.takeError();
-      ASSERT_EQ(1u, RenameResult->GlobalChanges.size());
-      EXPECT_EQ(
-          applyEdits(std::move(RenameResult->GlobalChanges)).front().second,
-          T.Expected);
+      if (std::optional<StringRef> Expected = T.Expected) {
+        ASSERT_TRUE(bool(RenameResult)) << RenameResult.takeError();
+        ASSERT_EQ(1u, RenameResult->GlobalChanges.size());
+        EXPECT_EQ(
+                  applyEdits(std::move(RenameResult->GlobalChanges)).front().second,
+                  *Expected);
+      } else {
+        ASSERT_FALSE(bool(RenameResult));
+        consumeError(RenameResult.takeError());
+      }
     }
   }
 }
@@ -1186,7 +1288,7 @@ TEST(RenameTest, Renameable) {
           int [[V^ar]];
         }
       )cpp",
-        nullptr, !HeaderFile},
+       nullptr, !HeaderFile},
   };
 
   for (const auto& Case : Cases) {
@@ -1808,6 +1910,147 @@ TEST(CrossFileRenameTests, WithUpToDateIndex) {
   }
 }
 
+TEST(CrossFileRenameTests, ObjC) {
+  MockCompilationDatabase CDB;
+  CDB.ExtraClangFlags = {"-xobjective-c"};
+  // rename is runnning on all "^" points in FooH, and "[[]]" ranges are the
+  // expected rename occurrences.
+  struct Case {
+    llvm::StringRef FooH;
+    llvm::StringRef FooM;
+    llvm::StringRef NewName;
+    llvm::StringRef ExpectedFooH;
+    llvm::StringRef ExpectedFooM;
+  };
+  Case Cases[] = {
+    // --- Zero arg selector
+    {
+      // Input
+      R"cpp(
+        @interface Foo
+        - (int)performA^ction;
+        @end
+      )cpp",
+      R"cpp(
+        @implementation Foo
+        - (int)performAction {
+          [self performAction];
+        }
+        @end
+      )cpp",
+      // New name
+      "performNewAction",
+      // Expected
+      R"cpp(
+        @interface Foo
+        - (int)performNewAction;
+        @end
+      )cpp",
+      R"cpp(
+        @implementation Foo
+        - (int)performNewAction {
+          [self performNewAction];
+        }
+        @end
+      )cpp",
+    },
+    // --- Single arg selector
+    {
+      // Input
+      R"cpp(
+        @interface Foo
+        - (int)performA^ction:(int)action;
+        @end
+      )cpp",
+      R"cpp(
+        @implementation Foo
+        - (int)performAction:(int)action {
+          [self performAction:action];
+        }
+        @end
+      )cpp",
+      // New name
+      "performNewAction:",
+      // Expected
+      R"cpp(
+        @interface Foo
+        - (int)performNewAction:(int)action;
+        @end
+      )cpp",
+      R"cpp(
+        @implementation Foo
+        - (int)performNewAction:(int)action {
+          [self performNewAction:action];
+        }
+        @end
+      )cpp",
+    },
+    // --- Multi arg selector
+    {
+      // Input
+      R"cpp(
+        @interface Foo
+        - (int)performA^ction:(int)action with:(int)value;
+        @end
+      )cpp",
+      R"cpp(
+        @implementation Foo
+        - (int)performAction:(int)action with:(int)value {
+          [self performAction:action with:value];
+        }
+        @end
+      )cpp",
+      // New name
+      "performNewAction:by:",
+      // Expected
+      R"cpp(
+        @interface Foo
+        - (int)performNewAction:(int)action by:(int)value;
+        @end
+      )cpp",
+      R"cpp(
+        @implementation Foo
+        - (int)performNewAction:(int)action by:(int)value {
+          [self performNewAction:action by:value];
+        }
+        @end
+      )cpp",
+    }
+  };
+
+  trace::TestTracer Tracer;
+  for (const auto &T : Cases) {
+    SCOPED_TRACE(T.FooH);
+    Annotations FooH(T.FooH);
+    Annotations FooM(T.FooM);
+    std::string FooHPath = testPath("foo.h");
+    std::string FooMPath = testPath("foo.m");
+
+    MockFS FS;
+    FS.Files[FooHPath] = std::string(FooH.code());
+    FS.Files[FooMPath] = std::string(FooM.code());
+
+    auto ServerOpts = ClangdServer::optsForTest();
+    ServerOpts.BuildDynamicSymbolIndex = true;
+    ClangdServer Server(CDB, FS, ServerOpts);
+
+    // Add all files to clangd server to make sure the dynamic index has been
+    // built.
+    runAddDocument(Server, FooHPath, FooH.code());
+    runAddDocument(Server, FooMPath, FooM.code());
+
+    for (const auto &RenamePos : FooH.points()) {
+      EXPECT_THAT(Tracer.takeMetric("rename_files"), SizeIs(0));
+      auto FileEditsList =
+          llvm::cantFail(runRename(Server, FooHPath, RenamePos, T.NewName, {}));
+      EXPECT_THAT(Tracer.takeMetric("rename_files"), ElementsAre(2));
+      EXPECT_THAT(applyEdits(std::move(FileEditsList.GlobalChanges)),
+                  UnorderedElementsAre(Pair(Eq(FooHPath), Eq(T.ExpectedFooH)),
+                                       Pair(Eq(FooMPath), Eq(T.ExpectedFooM))));
+    }
+  }
+}
+
 TEST(CrossFileRenameTests, CrossFileOnLocalSymbol) {
   // cross-file rename should work for function-local symbols, even there is no
   // index provided.
@@ -1828,7 +2071,13 @@ TEST(CrossFileRenameTests, BuildRenameEdits) {
   auto LSPRange = Code.range();
   llvm::StringRef FilePath = "/test/TestTU.cpp";
   llvm::StringRef NewName = "abc";
-  auto Edit = buildRenameEdit(FilePath, Code.code(), {LSPRange}, NewName);
+  tooling::SymbolName NewSymbolName(NewName, /*IsObjectiveCSelector=*/false);
+  tooling::SymbolName OldSymbolName("😂", /*IsObjectiveCSelector=*/false);
+
+  syntax::UnexpandedTokenBuffer Tokens(Code.code(), LangOptions());
+
+  auto Edit = buildRenameEdit(FilePath, Code.code(), {LSPRange}, OldSymbolName,
+                              NewSymbolName, Tokens);
   ASSERT_TRUE(bool(Edit)) << Edit.takeError();
   ASSERT_EQ(1UL, Edit->Replacements.size());
   EXPECT_EQ(FilePath, Edit->Replacements.begin()->getFilePath());
@@ -1836,7 +2085,8 @@ TEST(CrossFileRenameTests, BuildRenameEdits) {
 
   // Test invalid range.
   LSPRange.end = {10, 0}; // out of range
-  Edit = buildRenameEdit(FilePath, Code.code(), {LSPRange}, NewName);
+  Edit = buildRenameEdit(FilePath, Code.code(), {LSPRange}, OldSymbolName,
+                         NewSymbolName, Tokens);
   EXPECT_FALSE(Edit);
   EXPECT_THAT(llvm::toString(Edit.takeError()),
               testing::HasSubstr("fail to convert"));
@@ -1847,7 +2097,8 @@ TEST(CrossFileRenameTests, BuildRenameEdits) {
               [[range]]
       [[range]]
   )cpp");
-  Edit = buildRenameEdit(FilePath, T.code(), T.ranges(), NewName);
+  Edit = buildRenameEdit(FilePath, T.code(), T.ranges(), OldSymbolName,
+                         NewSymbolName, Tokens);
   ASSERT_TRUE(bool(Edit)) << Edit.takeError();
   EXPECT_EQ(applyEdits(FileEdits{{T.code(), std::move(*Edit)}}).front().second,
             expectedResult(T, NewName));
@@ -1904,8 +2155,9 @@ TEST(CrossFileRenameTests, adjustRenameRanges) {
   for (const auto &T : Tests) {
     SCOPED_TRACE(T.DraftCode);
     Annotations Draft(T.DraftCode);
-    auto ActualRanges = adjustRenameRanges(
-        Draft.code(), "x", Annotations(T.IndexedCode).ranges(), LangOpts);
+    syntax::UnexpandedTokenBuffer Tokens(Draft.code(), LangOpts);
+    auto ActualRanges =
+        adjustRenameRanges(Tokens, "x", Annotations(T.IndexedCode).ranges());
     if (!ActualRanges)
        EXPECT_THAT(Draft.ranges(), testing::IsEmpty());
     else
diff --git a/clang-tools-extra/clangd/unittests/SourceCodeTests.cpp b/clang-tools-extra/clangd/unittests/SourceCodeTests.cpp
index 08abde87df6d4..e22bd817f9a42 100644
--- a/clang-tools-extra/clangd/unittests/SourceCodeTests.cpp
+++ b/clang-tools-extra/clangd/unittests/SourceCodeTests.cpp
@@ -772,8 +772,8 @@ o foo2;
   )cpp");
   LangOptions LangOpts;
   LangOpts.CPlusPlus = true;
-  EXPECT_EQ(Code.ranges(),
-            collectIdentifierRanges("Foo", Code.code(), LangOpts));
+  syntax::UnexpandedTokenBuffer Tokens(Code.code(), LangOpts);
+  EXPECT_EQ(Code.ranges(), collectIdentifierRanges("Foo", Tokens));
 }
 
 TEST(SourceCodeTests, isHeaderFile) {
diff --git a/clang/include/clang/Tooling/Syntax/Tokens.h b/clang/include/clang/Tooling/Syntax/Tokens.h
index b1bdefed7c97f..8e057687d2e6a 100644
--- a/clang/include/clang/Tooling/Syntax/Tokens.h
+++ b/clang/include/clang/Tooling/Syntax/Tokens.h
@@ -145,6 +145,20 @@ class Token {
 /// For debugging purposes. Equivalent to a call to Token::str().
 llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const Token &T);
 
+/// A list of tokens as lexed from the input file, without expanding
+/// preprocessor macros.
+class UnexpandedTokenBuffer {
+  std::string Code;
+  std::vector<syntax::Token> Tokens;
+  std::unique_ptr<SourceManagerForFile> SrcMgr;
+
+public:
+  UnexpandedTokenBuffer(StringRef Code, const LangOptions &LangOpts);
+
+  ArrayRef<syntax::Token> tokens() const { return Tokens; }
+  const SourceManager &sourceManager() const { return SrcMgr->get(); }
+};
+
 /// A list of tokens obtained by preprocessing a text buffer and operations to
 /// map between the expanded and spelled tokens, i.e. TokenBuffer has
 /// information about two token streams:
diff --git a/clang/lib/Tooling/Refactoring/SymbolName.cpp b/clang/lib/Tooling/Refactoring/SymbolName.cpp
index be0dd721b1d22..b136e8a59bd23 100644
--- a/clang/lib/Tooling/Refactoring/SymbolName.cpp
+++ b/clang/lib/Tooling/Refactoring/SymbolName.cpp
@@ -16,7 +16,9 @@ namespace tooling {
 SymbolName::SymbolName(const DeclarationName &DeclName)
     : SymbolName(DeclName.getAsString(),
                  /*IsObjectiveCSelector=*/DeclName.getNameKind() ==
-                     DeclarationName::NameKind::ObjCMultiArgSelector) {}
+                         DeclarationName::NameKind::ObjCMultiArgSelector ||
+                     DeclName.getNameKind() ==
+                         DeclarationName::NameKind::ObjCOneArgSelector) {}
 
 SymbolName::SymbolName(StringRef Name, const LangOptions &LangOpts)
     : SymbolName(Name, LangOpts.ObjC) {}
diff --git a/clang/lib/Tooling/Syntax/Tokens.cpp b/clang/lib/Tooling/Syntax/Tokens.cpp
index 8d32c45a4a70c..c85edf82518a7 100644
--- a/clang/lib/Tooling/Syntax/Tokens.cpp
+++ b/clang/lib/Tooling/Syntax/Tokens.cpp
@@ -225,6 +225,17 @@ llvm::StringRef FileRange::text(const SourceManager &SM) const {
   return Text.substr(Begin, length());
 }
 
+UnexpandedTokenBuffer::UnexpandedTokenBuffer(StringRef Code,
+                                             const LangOptions &LangOpts) {
+  // InMemoryFileAdapter crashes unless the buffer is null terminated, so ensure
+  // the string is null-terminated.
+  this->Code = Code.str();
+  SrcMgr =
+      std::make_unique<SourceManagerForFile>("mock_file_name.cpp", this->Code);
+  Tokens = syntax::tokenize(sourceManager().getMainFileID(), sourceManager(),
+                            LangOpts);
+}
+
 void TokenBuffer::indexExpandedTokens() {
   // No-op if the index is already created.
   if (!ExpandedTokIndex.empty())

>From 8ce6dd84fbae77420ee0fbd2ee27840b35aabfae Mon Sep 17 00:00:00 2001
From: Alex Hoppen <ahoppen at apple.com>
Date: Wed, 20 Dec 2023 12:02:27 -0800
Subject: [PATCH 4/5] [clangd] Return a placeholder string for the
 `prepareRename` request

This allows us to return the old name of an Objective-C method, which pre-populates the new name field in an editor.
---
 clang-tools-extra/clangd/ClangdLSPServer.cpp  |  5 +--
 clang-tools-extra/clangd/ClangdLSPServer.h    |  2 +-
 clang-tools-extra/clangd/ClangdServer.cpp     |  3 +-
 clang-tools-extra/clangd/Protocol.cpp         |  4 +++
 clang-tools-extra/clangd/Protocol.h           | 11 ++++++
 clang-tools-extra/clangd/refactor/Rename.cpp  | 16 ++++++++-
 clang-tools-extra/clangd/refactor/Rename.h    | 18 ++++++----
 clang-tools-extra/clangd/test/rename.test     |  3 ++
 .../clangd/unittests/RenameTests.cpp          | 34 +++++++++++++++++++
 .../Tooling/Refactoring/Rename/SymbolName.h   |  1 +
 clang/lib/Tooling/Refactoring/SymbolName.cpp  |  5 +++
 11 files changed, 90 insertions(+), 12 deletions(-)

diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index a87da252b7a7e..5dfd12045b657 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -844,14 +844,15 @@ void ClangdLSPServer::onWorkspaceSymbol(
 }
 
 void ClangdLSPServer::onPrepareRename(const TextDocumentPositionParams &Params,
-                                      Callback<std::optional<Range>> Reply) {
+                                      Callback<PrepareRenameResult> Reply) {
   Server->prepareRename(
       Params.textDocument.uri.file(), Params.position, /*NewName*/ std::nullopt,
       Opts.Rename,
       [Reply = std::move(Reply)](llvm::Expected<RenameResult> Result) mutable {
         if (!Result)
           return Reply(Result.takeError());
-        return Reply(std::move(Result->Target));
+        PrepareRenameResult PrepareResult{Result->Target, Result->OldName};
+        return Reply(std::move(PrepareResult));
       });
 }
 
diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h
index 79579c22b788a..6a9f097d551ae 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.h
+++ b/clang-tools-extra/clangd/ClangdLSPServer.h
@@ -134,7 +134,7 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
   void onWorkspaceSymbol(const WorkspaceSymbolParams &,
                          Callback<std::vector<SymbolInformation>>);
   void onPrepareRename(const TextDocumentPositionParams &,
-                       Callback<std::optional<Range>>);
+                       Callback<PrepareRenameResult>);
   void onRename(const RenameParams &, Callback<WorkspaceEdit>);
   void onHover(const TextDocumentPositionParams &,
                Callback<std::optional<Hover>>);
diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp
index 6fb2641e8793d..b04ebc7049c66 100644
--- a/clang-tools-extra/clangd/ClangdServer.cpp
+++ b/clang-tools-extra/clangd/ClangdServer.cpp
@@ -578,8 +578,7 @@ void ClangdServer::prepareRename(PathRef File, Position Pos,
     // prepareRename is latency-sensitive: we don't query the index, as we
     // only need main-file references
     auto Results =
-        clangd::rename({Pos, NewName.value_or("__clangd_rename_placeholder"),
-                        InpAST->AST, File, /*FS=*/nullptr,
+        clangd::rename({Pos, NewName, InpAST->AST, File, /*FS=*/nullptr,
                         /*Index=*/nullptr, RenameOpts});
     if (!Results) {
       // LSP says to return null on failure, but that will result in a generic
diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp
index a6370649f5ad1..0291e5d71d65c 100644
--- a/clang-tools-extra/clangd/Protocol.cpp
+++ b/clang-tools-extra/clangd/Protocol.cpp
@@ -905,6 +905,10 @@ llvm::json::Value toJSON(const DocumentSymbol &S) {
   return std::move(Result);
 }
 
+llvm::json::Value toJSON(const PrepareRenameResult &R) {
+  return llvm::json::Object{{"range", R.range}, {"placeholder", R.placeholder}};
+}
+
 llvm::json::Value toJSON(const WorkspaceEdit &WE) {
   llvm::json::Object Result;
   if (WE.changes) {
diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h
index e88c804692568..65366a7f1307b 100644
--- a/clang-tools-extra/clangd/Protocol.h
+++ b/clang-tools-extra/clangd/Protocol.h
@@ -1004,6 +1004,17 @@ struct CodeActionParams {
 };
 bool fromJSON(const llvm::json::Value &, CodeActionParams &, llvm::json::Path);
 
+struct PrepareRenameResult {
+  /// The range of the string to rename.
+  Range range;
+  /// A placeholder text of the string content to be renamed.
+  ///
+  /// This is useful to populate the rename field with an Objective-C selector
+  /// name (eg. `performAction:with:`) when renaming Objective-C methods.
+  std::string placeholder;
+};
+llvm::json::Value toJSON(const PrepareRenameResult &WE);
+
 /// The edit should either provide changes or documentChanges. If the client
 /// can handle versioned document edits and if documentChanges are present,
 /// the latter are preferred over changes.
diff --git a/clang-tools-extra/clangd/refactor/Rename.cpp b/clang-tools-extra/clangd/refactor/Rename.cpp
index c6af9c137751f..51a809927c670 100644
--- a/clang-tools-extra/clangd/refactor/Rename.cpp
+++ b/clang-tools-extra/clangd/refactor/Rename.cpp
@@ -811,7 +811,20 @@ llvm::Expected<RenameResult> rename(const RenameInputs &RInputs) {
   if (!Name)
     return makeError(ReasonToReject::UnsupportedSymbol);
   SymbolName OldSymbolName(Name);
-  SymbolName NewSymbolName(RInputs.NewName, AST.getLangOpts());
+  SymbolName NewSymbolName(ArrayRef<StringRef>{});
+  if (std::optional<StringRef> NewName = RInputs.NewName) {
+    NewSymbolName = SymbolName(*NewName, AST.getLangOpts());
+  } else {
+    // If no new name is given, we are perfoming a pseudo rename for the
+    // prepareRename request to check if the rename is possible. Construct a
+    // new symbol name that has as many name pieces as the old name and is thus
+    // a valid new name.
+    std::vector<std::string> NewNamePieces;
+    for (StringRef Piece : OldSymbolName.getNamePieces()) {
+      NewNamePieces.push_back(Piece.str() + "__clangd_rename_placeholder");
+    }
+    NewSymbolName = SymbolName(NewNamePieces);
+  }
   if (OldSymbolName == NewSymbolName)
     return makeError(ReasonToReject::SameName);
   auto Invalid = checkName(RenameDecl, NewSymbolName);
@@ -858,6 +871,7 @@ llvm::Expected<RenameResult> rename(const RenameInputs &RInputs) {
   }
   RenameResult Result;
   Result.Target = CurrentIdentifier;
+  Result.OldName = RenameDecl.getNameAsString();
   Edit MainFileEdits = Edit(MainFileCode, std::move(*MainFileRenameEdit));
   for (const TextEdit &TE : MainFileEdits.asTextEdits())
     Result.LocalChanges.push_back(TE.range);
diff --git a/clang-tools-extra/clangd/refactor/Rename.h b/clang-tools-extra/clangd/refactor/Rename.h
index f14f20a6e1759..b7ca7e885fc90 100644
--- a/clang-tools-extra/clangd/refactor/Rename.h
+++ b/clang-tools-extra/clangd/refactor/Rename.h
@@ -35,8 +35,12 @@ struct RenameOptions {
 };
 
 struct RenameInputs {
-  Position Pos; // the position triggering the rename
-  llvm::StringRef NewName;
+  /// The position triggering the rename
+  Position Pos;
+
+  /// The new name to give to the symbol or `nullopt` to perform a fake rename
+  /// that checks if rename is possible.
+  std::optional<llvm::StringRef> NewName;
 
   ParsedAST &AST;
   llvm::StringRef MainFilePath;
@@ -52,12 +56,14 @@ struct RenameInputs {
 };
 
 struct RenameResult {
-  // The range of the symbol that the user can attempt to rename.
+  /// The range of the symbol that the user can attempt to rename.
   Range Target;
-  // Rename occurrences for the current main file.
+  /// The current name of the declaration at the cursor.
+  std::string OldName;
+  /// Rename occurrences for the current main file.
   std::vector<Range> LocalChanges;
-  // Complete edits for the rename, including LocalChanges.
-  // If the full set of changes is unknown, this field is empty.
+  /// Complete edits for the rename, including LocalChanges.
+  /// If the full set of changes is unknown, this field is empty.
   FileEdits GlobalChanges;
 };
 
diff --git a/clang-tools-extra/clangd/test/rename.test b/clang-tools-extra/clangd/test/rename.test
index 527b4263443a7..0dc1a103002b2 100644
--- a/clang-tools-extra/clangd/test/rename.test
+++ b/clang-tools-extra/clangd/test/rename.test
@@ -10,6 +10,8 @@
 #      CHECK:  "id": 1,
 # CHECK-NEXT:  "jsonrpc": "2.0",
 # CHECK-NEXT:  "result": {
+# CHECK-NEXT:    "placeholder": "foo",
+# CHECK-NEXT:    "range": {
 # CHECK-NEXT:      "end": {
 # CHECK-NEXT:        "character": 7,
 # CHECK-NEXT:        "line": 0
@@ -18,6 +20,7 @@
 # CHECK-NEXT:        "character": 4,
 # CHECK-NEXT:        "line": 0
 # CHECK-NEXT:      }
+# CHECK-NEXT:    }
 # CHECK-NEXT:  }
 ---
 {"jsonrpc":"2.0","id":2,"method":"textDocument/prepareRename","params":{"textDocument":{"uri":"test:///foo.cpp"},"position":{"line":0,"character":2}}}
diff --git a/clang-tools-extra/clangd/unittests/RenameTests.cpp b/clang-tools-extra/clangd/unittests/RenameTests.cpp
index 008639d573da9..94e5917ea6bc5 100644
--- a/clang-tools-extra/clangd/unittests/RenameTests.cpp
+++ b/clang-tools-extra/clangd/unittests/RenameTests.cpp
@@ -1476,6 +1476,7 @@ TEST(RenameTest, PrepareRename) {
                                   /*NewName=*/std::nullopt, {});
   // Verify that for multi-file rename, we only return main-file occurrences.
   ASSERT_TRUE(bool(Results)) << Results.takeError();
+  ASSERT_EQ(Results->OldName, "func");
   // We don't know the result is complete in prepareRename (passing a nullptr
   // index internally), so GlobalChanges should be empty.
   EXPECT_TRUE(Results->GlobalChanges.empty());
@@ -1507,6 +1508,39 @@ TEST(RenameTest, PrepareRename) {
   }
 }
 
+TEST(RenameTest, PrepareRenameObjC) {
+  Annotations Input(R"cpp(
+    @interface Foo
+    - (int)performA^ction:(int)action w^ith:(int)value;
+    @end
+    @implementation Foo
+    - (int)performA^ction:(int)action w^ith:(int)value {
+      return [self ^performAction^:action ^w^ith^:value];
+    }
+    @end
+  )cpp");
+  std::string Path = testPath("foo.m");
+  MockFS FS;
+  FS.Files[Path] = std::string(Input.code());
+
+  auto ServerOpts = ClangdServer::optsForTest();
+  ServerOpts.BuildDynamicSymbolIndex = true;
+
+  trace::TestTracer Tracer;
+  MockCompilationDatabase CDB;
+  CDB.ExtraClangFlags = {"-xobjective-c"};
+  ClangdServer Server(CDB, FS, ServerOpts);
+  runAddDocument(Server, Path, Input.code());
+
+  for (Position Point : Input.points()) {
+    auto Results = runPrepareRename(Server, Path, Point,
+                                    /*NewName=*/std::nullopt, {});
+    // Verify that for multi-file rename, we only return main-file occurrences.
+    ASSERT_TRUE(bool(Results)) << Results.takeError();
+    ASSERT_EQ(Results->OldName, "performAction:with:");
+  }
+}
+
 TEST(CrossFileRenameTests, DirtyBuffer) {
   Annotations FooCode("class [[Foo]] {};");
   std::string FooPath = testPath("foo.cc");
diff --git a/clang/include/clang/Tooling/Refactoring/Rename/SymbolName.h b/clang/include/clang/Tooling/Refactoring/Rename/SymbolName.h
index 31520ecabb31a..8f22cdbba8ee5 100644
--- a/clang/include/clang/Tooling/Refactoring/Rename/SymbolName.h
+++ b/clang/include/clang/Tooling/Refactoring/Rename/SymbolName.h
@@ -36,6 +36,7 @@ class SymbolName {
 public:
   /// Create a new \c SymbolName with the specified pieces.
   explicit SymbolName(ArrayRef<StringRef> NamePieces);
+  explicit SymbolName(ArrayRef<std::string> NamePieces);
 
   explicit SymbolName(const DeclarationName &Name);
 
diff --git a/clang/lib/Tooling/Refactoring/SymbolName.cpp b/clang/lib/Tooling/Refactoring/SymbolName.cpp
index b136e8a59bd23..fbfdc41f2c8b3 100644
--- a/clang/lib/Tooling/Refactoring/SymbolName.cpp
+++ b/clang/lib/Tooling/Refactoring/SymbolName.cpp
@@ -41,6 +41,11 @@ SymbolName::SymbolName(ArrayRef<StringRef> NamePieces) {
     this->NamePieces.push_back(Piece.str());
 }
 
+SymbolName::SymbolName(ArrayRef<std::string> NamePieces) {
+  for (const auto &Piece : NamePieces)
+    this->NamePieces.push_back(Piece);
+}
+
 std::optional<std::string> SymbolName::getSinglePiece() const {
   if (getNamePieces().size() == 1) {
     return NamePieces.front();

>From 5e4853d277dd94a414d2dbd050f98f3cec6a27ec Mon Sep 17 00:00:00 2001
From: Alex Hoppen <ahoppen at apple.com>
Date: Wed, 10 Jan 2024 14:37:07 -0800
Subject: [PATCH 5/5] [clangd] Address review comments to support rename of
 Objective-C selectors

---
 clang-tools-extra/clangd/refactor/Rename.cpp  | 102 +++++++++---------
 clang-tools-extra/clangd/refactor/Rename.h    |   2 +-
 .../clangd/unittests/RenameTests.cpp          |   4 +-
 .../Refactoring/Rename/RenamingAction.h       |   4 +-
 .../Tooling/Refactoring/Rename/SymbolName.h   |   2 +
 clang/include/clang/Tooling/Syntax/Tokens.h   |   1 -
 .../Refactoring/Rename/RenamingAction.cpp     |  20 ++--
 clang/lib/Tooling/Refactoring/SymbolName.cpp  |  11 +-
 clang/lib/Tooling/Syntax/Tokens.cpp           |   6 +-
 .../Tooling/RefactoringActionRulesTest.cpp    |   6 +-
 10 files changed, 77 insertions(+), 81 deletions(-)

diff --git a/clang-tools-extra/clangd/refactor/Rename.cpp b/clang-tools-extra/clangd/refactor/Rename.cpp
index 51a809927c670..f639cbc9b4b12 100644
--- a/clang-tools-extra/clangd/refactor/Rename.cpp
+++ b/clang-tools-extra/clangd/refactor/Rename.cpp
@@ -526,23 +526,25 @@ std::optional<InvalidName> checkName(const NamedDecl &RenameDecl,
   static constexpr trace::Metric InvalidNameMetric(
       "rename_name_invalid", trace::Metric::Counter, "invalid_kind");
   auto &ASTCtx = RenameDecl.getASTContext();
+  auto Identifier = NewName.getSinglePiece();
+  if (!Identifier) {
+    return std::nullopt;
+  }
   std::optional<InvalidName> Result;
-  if (auto Identifier = NewName.getSinglePiece()) {
-    if (isKeyword(*Identifier, ASTCtx.getLangOpts()))
-      Result = InvalidName{InvalidName::Keywords, *Identifier};
-    else if (!mayBeValidIdentifier(*Identifier))
-      Result = InvalidName{InvalidName::BadIdentifier, *Identifier};
-    else {
-      // Name conflict detection.
-      // Function conflicts are subtle (overloading), so ignore them.
-      if (RenameDecl.getKind() != Decl::Function &&
-          RenameDecl.getKind() != Decl::CXXMethod) {
-        if (auto *Conflict =
-                lookupSiblingWithName(ASTCtx, RenameDecl, *Identifier))
-          Result = InvalidName{
-              InvalidName::Conflict,
-              Conflict->getLocation().printToString(ASTCtx.getSourceManager())};
-      }
+  if (isKeyword(*Identifier, ASTCtx.getLangOpts())) {
+    Result = InvalidName{InvalidName::Keywords, *Identifier};
+  } else if (!mayBeValidIdentifier(*Identifier)) {
+    Result = InvalidName{InvalidName::BadIdentifier, *Identifier};
+  } else {
+    // Name conflict detection.
+    // Function conflicts are subtle (overloading), so ignore them.
+    if (RenameDecl.getKind() != Decl::Function &&
+        RenameDecl.getKind() != Decl::CXXMethod) {
+      if (auto *Conflict =
+              lookupSiblingWithName(ASTCtx, RenameDecl, *Identifier))
+        Result = InvalidName{
+            InvalidName::Conflict,
+            Conflict->getLocation().printToString(ASTCtx.getSourceManager())};
     }
   }
   if (Result)
@@ -579,24 +581,26 @@ renameWithinFile(ParsedAST &AST, const NamedDecl &RenameDecl,
     if (std::optional<std::string> Identifier = NewName.getSinglePiece()) {
       tooling::Replacement NewReplacement(
           SM, CharSourceRange::getTokenRange(RenameLoc), *Identifier);
-      if (auto Err = FilteredChanges.add(NewReplacement))
+      if (auto Err = FilteredChanges.add(NewReplacement)) {
         return std::move(Err);
-    } else {
-      llvm::Expected<SmallVector<SourceLocation>> PieceLocations =
-          findObjCSymbolSelectorPieces(
-              AST.getTokens().expandedTokens(), SM, RenameLoc, OldName,
-              tooling::ObjCSymbolSelectorKind::Unknown);
-      if (!PieceLocations) {
-        return PieceLocations.takeError();
-      }
-      assert(PieceLocations->size() == NewName.getNamePieces().size());
-      for (auto [Location, NewPiece] :
-           llvm::zip_equal(*PieceLocations, NewName.getNamePieces())) {
-        tooling::Replacement NewReplacement(
-            SM, CharSourceRange::getTokenRange(Location), NewPiece);
-        if (auto Err = FilteredChanges.add(NewReplacement))
-          return std::move(Err);
       }
+      continue;
+    }
+    SmallVector<SourceLocation> PieceLocations;
+    llvm::Error Error = findObjCSymbolSelectorPieces(
+        AST.getTokens().expandedTokens(), SM, RenameLoc, OldName,
+        tooling::ObjCSymbolSelectorKind::Unknown, PieceLocations);
+    if (Error) {
+      // Ignore the error. We simply skip over all selectors that didn't match.
+      consumeError(std::move(Error));
+      continue;
+    }
+    for (auto [Location, NewPiece] :
+         llvm::zip_equal(PieceLocations, NewName.getNamePieces())) {
+      tooling::Replacement NewReplacement(
+          SM, CharSourceRange::getTokenRange(Location), NewPiece);
+      if (auto Err = FilteredChanges.add(NewReplacement))
+        return std::move(Err);
     }
   }
   return FilteredChanges;
@@ -811,18 +815,16 @@ llvm::Expected<RenameResult> rename(const RenameInputs &RInputs) {
   if (!Name)
     return makeError(ReasonToReject::UnsupportedSymbol);
   SymbolName OldSymbolName(Name);
-  SymbolName NewSymbolName(ArrayRef<StringRef>{});
-  if (std::optional<StringRef> NewName = RInputs.NewName) {
-    NewSymbolName = SymbolName(*NewName, AST.getLangOpts());
+  SymbolName NewSymbolName;
+  if (RInputs.NewName) {
+    NewSymbolName = SymbolName(*RInputs.NewName, AST.getLangOpts());
   } else {
     // If no new name is given, we are perfoming a pseudo rename for the
     // prepareRename request to check if the rename is possible. Construct a
     // new symbol name that has as many name pieces as the old name and is thus
     // a valid new name.
-    std::vector<std::string> NewNamePieces;
-    for (StringRef Piece : OldSymbolName.getNamePieces()) {
-      NewNamePieces.push_back(Piece.str() + "__clangd_rename_placeholder");
-    }
+    std::vector<std::string> NewNamePieces = OldSymbolName.getNamePieces();
+    NewNamePieces[0] += "__clangd_rename_placeholder";
     NewSymbolName = SymbolName(NewNamePieces);
   }
   if (OldSymbolName == NewSymbolName)
@@ -961,18 +963,20 @@ buildRenameEdit(llvm::StringRef AbsFilePath, llvm::StringRef InitialCode,
               AbsFilePath, R.first, ByteLength, *Identifier)))
         return std::move(Err);
     } else {
-      llvm::Expected<SmallVector<SourceLocation>> PieceLocations =
-          findObjCSymbolSelectorPieces(
-              Tokens.tokens(), SM,
-              SM.getLocForStartOfFile(SM.getMainFileID())
-                  .getLocWithOffset(R.first),
-              OldName, tooling::ObjCSymbolSelectorKind::Unknown);
-      if (!PieceLocations) {
-        return PieceLocations.takeError();
+      SmallVector<SourceLocation> PieceLocations;
+      llvm::Error Error = findObjCSymbolSelectorPieces(
+          Tokens.tokens(), SM,
+          SM.getLocForStartOfFile(SM.getMainFileID()).getLocWithOffset(R.first),
+          OldName, tooling::ObjCSymbolSelectorKind::Unknown, PieceLocations);
+      if (Error) {
+        // Ignore the error. We simply skip over all selectors that didn't
+        // match.
+        consumeError(std::move(Error));
+        continue;
       }
-      assert(PieceLocations->size() == NewName.getNamePieces().size());
+      assert(PieceLocations.size() == NewName.getNamePieces().size());
       for (auto [Location, NewPiece] :
-           llvm::zip_equal(*PieceLocations, NewName.getNamePieces())) {
+           llvm::zip_equal(PieceLocations, NewName.getNamePieces())) {
         tooling::Replacement NewReplacement(
             SM, CharSourceRange::getTokenRange(Location), NewPiece);
         if (auto Err = RenameEdit.add(NewReplacement))
diff --git a/clang-tools-extra/clangd/refactor/Rename.h b/clang-tools-extra/clangd/refactor/Rename.h
index b7ca7e885fc90..5b43875bf56a3 100644
--- a/clang-tools-extra/clangd/refactor/Rename.h
+++ b/clang-tools-extra/clangd/refactor/Rename.h
@@ -75,7 +75,7 @@ llvm::Expected<RenameResult> rename(const RenameInputs &RInputs);
 /// Generates rename edits that replaces all given occurrences with the
 /// `NewName`.
 ///
-/// `OldName` is and `Tokens` are used to to find the argument labels of
+/// `OldName` and `Tokens` are used to to find the argument labels of
 /// Objective-C selectors.
 ///
 /// Exposed for testing only.
diff --git a/clang-tools-extra/clangd/unittests/RenameTests.cpp b/clang-tools-extra/clangd/unittests/RenameTests.cpp
index 94e5917ea6bc5..ff64e1764513e 100644
--- a/clang-tools-extra/clangd/unittests/RenameTests.cpp
+++ b/clang-tools-extra/clangd/unittests/RenameTests.cpp
@@ -1535,7 +1535,6 @@ TEST(RenameTest, PrepareRenameObjC) {
   for (Position Point : Input.points()) {
     auto Results = runPrepareRename(Server, Path, Point,
                                     /*NewName=*/std::nullopt, {});
-    // Verify that for multi-file rename, we only return main-file occurrences.
     ASSERT_TRUE(bool(Results)) << Results.takeError();
     ASSERT_EQ(Results->OldName, "performAction:with:");
   }
@@ -1947,8 +1946,7 @@ TEST(CrossFileRenameTests, WithUpToDateIndex) {
 TEST(CrossFileRenameTests, ObjC) {
   MockCompilationDatabase CDB;
   CDB.ExtraClangFlags = {"-xobjective-c"};
-  // rename is runnning on all "^" points in FooH, and "[[]]" ranges are the
-  // expected rename occurrences.
+  // rename is runnning on all "^" points in FooH.
   struct Case {
     llvm::StringRef FooH;
     llvm::StringRef FooM;
diff --git a/clang/include/clang/Tooling/Refactoring/Rename/RenamingAction.h b/clang/include/clang/Tooling/Refactoring/Rename/RenamingAction.h
index 79c2b384b4eb0..e6b38c40c8182 100644
--- a/clang/include/clang/Tooling/Refactoring/Rename/RenamingAction.h
+++ b/clang/include/clang/Tooling/Refactoring/Rename/RenamingAction.h
@@ -132,10 +132,10 @@ enum class ObjCSymbolSelectorKind {
   Unknown
 };
 
-llvm::Expected<SmallVector<SourceLocation>> findObjCSymbolSelectorPieces(
+llvm::Error findObjCSymbolSelectorPieces(
     ArrayRef<syntax::Token> Tokens, const SourceManager &SrcMgr,
     SourceLocation RenameLoc, const SymbolName &OldName,
-    ObjCSymbolSelectorKind Kind);
+    ObjCSymbolSelectorKind Kind, SmallVectorImpl<SourceLocation> &Result);
 
 } // end namespace tooling
 } // end namespace clang
diff --git a/clang/include/clang/Tooling/Refactoring/Rename/SymbolName.h b/clang/include/clang/Tooling/Refactoring/Rename/SymbolName.h
index 8f22cdbba8ee5..887ab0929445d 100644
--- a/clang/include/clang/Tooling/Refactoring/Rename/SymbolName.h
+++ b/clang/include/clang/Tooling/Refactoring/Rename/SymbolName.h
@@ -34,6 +34,8 @@ class SymbolName {
   llvm::SmallVector<std::string, 1> NamePieces;
 
 public:
+  SymbolName();
+
   /// Create a new \c SymbolName with the specified pieces.
   explicit SymbolName(ArrayRef<StringRef> NamePieces);
   explicit SymbolName(ArrayRef<std::string> NamePieces);
diff --git a/clang/include/clang/Tooling/Syntax/Tokens.h b/clang/include/clang/Tooling/Syntax/Tokens.h
index 8e057687d2e6a..f765788d4e857 100644
--- a/clang/include/clang/Tooling/Syntax/Tokens.h
+++ b/clang/include/clang/Tooling/Syntax/Tokens.h
@@ -148,7 +148,6 @@ llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const Token &T);
 /// A list of tokens as lexed from the input file, without expanding
 /// preprocessor macros.
 class UnexpandedTokenBuffer {
-  std::string Code;
   std::vector<syntax::Token> Tokens;
   std::unique_ptr<SourceManagerForFile> SrcMgr;
 
diff --git a/clang/lib/Tooling/Refactoring/Rename/RenamingAction.cpp b/clang/lib/Tooling/Refactoring/Rename/RenamingAction.cpp
index b76cb06bc8aa9..20802d3a5f101 100644
--- a/clang/lib/Tooling/Refactoring/Rename/RenamingAction.cpp
+++ b/clang/lib/Tooling/Refactoring/Rename/RenamingAction.cpp
@@ -285,11 +285,12 @@ static bool isMatchingSelectorName(const syntax::Token &Tok,
          Next.kind() == tok::colon && Tok.text(SrcMgr) == NamePiece;
 }
 
-llvm::Expected<SmallVector<SourceLocation>>
-findObjCSymbolSelectorPieces(ArrayRef<syntax::Token> AllTokens,
-                             const SourceManager &SM, SourceLocation RenameLoc,
-                             const SymbolName &OldName,
-                             ObjCSymbolSelectorKind Kind) {
+Error findObjCSymbolSelectorPieces(ArrayRef<syntax::Token> AllTokens,
+                                   const SourceManager &SM,
+                                   SourceLocation RenameLoc,
+                                   const SymbolName &OldName,
+                                   ObjCSymbolSelectorKind Kind,
+                                   SmallVectorImpl<SourceLocation> &Result) {
   ArrayRef<syntax::Token> Tokens =
       AllTokens.drop_while([RenameLoc](syntax::Token Tok) -> bool {
         return Tok.location() != RenameLoc;
@@ -298,7 +299,6 @@ findObjCSymbolSelectorPieces(ArrayRef<syntax::Token> AllTokens,
   assert(OldName.getNamePieces()[0].empty() ||
          Tokens[0].text(SM) == OldName.getNamePieces()[0]);
   assert(OldName.getNamePieces().size() > 1);
-  SmallVector<SourceLocation> Result;
 
   Result.push_back(Tokens[0].location());
 
@@ -328,7 +328,7 @@ findObjCSymbolSelectorPieces(ArrayRef<syntax::Token> AllTokens,
       Result.push_back(Tok.location());
       // All the selector pieces have been found.
       if (Result.size() == OldName.getNamePieces().size())
-        return Result;
+        return Error::success();
     } else if (Tok.kind() == tok::r_square) {
       // Stop scanning at the end of the message send.
       // Also account for spurious ']' in blocks or lambdas.
@@ -337,11 +337,11 @@ findObjCSymbolSelectorPieces(ArrayRef<syntax::Token> AllTokens,
         break;
       if (SquareCount)
         --SquareCount;
-    } else if (Tok.kind() == tok::l_square)
+    } else if (Tok.kind() == tok::l_square) {
       ++SquareCount;
-    else if (Tok.kind() == tok::l_paren)
+    } else if (Tok.kind() == tok::l_paren) {
       ++ParenCount;
-    else if (Tok.kind() == tok::r_paren) {
+    } else if (Tok.kind() == tok::r_paren) {
       if (!ParenCount)
         break;
       --ParenCount;
diff --git a/clang/lib/Tooling/Refactoring/SymbolName.cpp b/clang/lib/Tooling/Refactoring/SymbolName.cpp
index fbfdc41f2c8b3..896a6cf09a3a9 100644
--- a/clang/lib/Tooling/Refactoring/SymbolName.cpp
+++ b/clang/lib/Tooling/Refactoring/SymbolName.cpp
@@ -13,6 +13,8 @@
 namespace clang {
 namespace tooling {
 
+SymbolName::SymbolName() : NamePieces({}) {}
+
 SymbolName::SymbolName(const DeclarationName &DeclName)
     : SymbolName(DeclName.getAsString(),
                  /*IsObjectiveCSelector=*/DeclName.getNameKind() ==
@@ -49,9 +51,8 @@ SymbolName::SymbolName(ArrayRef<std::string> NamePieces) {
 std::optional<std::string> SymbolName::getSinglePiece() const {
   if (getNamePieces().size() == 1) {
     return NamePieces.front();
-  } else {
-    return std::nullopt;
   }
+  return std::nullopt;
 }
 
 std::string SymbolName::getAsString() const {
@@ -62,11 +63,7 @@ std::string SymbolName::getAsString() const {
 }
 
 void SymbolName::print(raw_ostream &OS) const {
-  for (size_t I = 0, E = NamePieces.size(); I != E; ++I) {
-    if (I != 0)
-      OS << ':';
-    OS << NamePieces[I];
-  }
+  llvm::interleave(NamePieces, OS, ":");
 }
 
 } // end namespace tooling
diff --git a/clang/lib/Tooling/Syntax/Tokens.cpp b/clang/lib/Tooling/Syntax/Tokens.cpp
index c85edf82518a7..1f7a2cb4f499b 100644
--- a/clang/lib/Tooling/Syntax/Tokens.cpp
+++ b/clang/lib/Tooling/Syntax/Tokens.cpp
@@ -227,11 +227,7 @@ llvm::StringRef FileRange::text(const SourceManager &SM) const {
 
 UnexpandedTokenBuffer::UnexpandedTokenBuffer(StringRef Code,
                                              const LangOptions &LangOpts) {
-  // InMemoryFileAdapter crashes unless the buffer is null terminated, so ensure
-  // the string is null-terminated.
-  this->Code = Code.str();
-  SrcMgr =
-      std::make_unique<SourceManagerForFile>("mock_file_name.cpp", this->Code);
+  SrcMgr = std::make_unique<SourceManagerForFile>("mock_file_name.cpp", Code);
   Tokens = syntax::tokenize(sourceManager().getMainFileID(), sourceManager(),
                             LangOpts);
 }
diff --git a/clang/unittests/Tooling/RefactoringActionRulesTest.cpp b/clang/unittests/Tooling/RefactoringActionRulesTest.cpp
index cdd1faf0e38d4..c405ea02f90f6 100644
--- a/clang/unittests/Tooling/RefactoringActionRulesTest.cpp
+++ b/clang/unittests/Tooling/RefactoringActionRulesTest.cpp
@@ -211,9 +211,9 @@ TEST_F(RefactoringActionRulesTest, ReturnSymbolOccurrences) {
     Expected<SymbolOccurrences>
     findSymbolOccurrences(RefactoringRuleContext &) override {
       SymbolOccurrences Occurrences;
-      Occurrences.push_back(SymbolOccurrence(SymbolName("test"),
-                                             SymbolOccurrence::MatchingSymbol,
-                                             Selection.getBegin()));
+      Occurrences.push_back(SymbolOccurrence(
+          SymbolName("test", /*IsObjectiveCSelector=*/false),
+          SymbolOccurrence::MatchingSymbol, Selection.getBegin()));
       return std::move(Occurrences);
     }
   };



More information about the cfe-commits mailing list