Skip to content

[clangd] Use SymbolName to represent Objective-C selectors #82061

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 70 additions & 31 deletions clang-tools-extra/clangd/refactor/Rename.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
#include "clang/AST/ASTContext.h"
#include "clang/AST/ASTTypeTraits.h"
#include "clang/AST/Decl.h"
#include "clang/AST/DeclBase.h"
#include "clang/AST/DeclCXX.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/DeclTemplate.h"
#include "clang/AST/DeclarationName.h"
#include "clang/AST/ParentMapContext.h"
#include "clang/AST/Stmt.h"
#include "clang/Basic/CharInfo.h"
Expand Down Expand Up @@ -597,11 +599,12 @@ bool isMatchingSelectorName(const syntax::Token &Cur, const syntax::Token &Next,
// The search will terminate upon seeing Terminator or a ; at the top level.
std::optional<SymbolRange>
findAllSelectorPieces(llvm::ArrayRef<syntax::Token> Tokens,
const SourceManager &SM, Selector Sel,
const SourceManager &SM, const RenameSymbolName &Name,
tok::TokenKind Terminator) {
assert(!Tokens.empty());

unsigned NumArgs = Sel.getNumArgs();
ArrayRef<std::string> NamePieces = Name.getNamePieces();
unsigned NumArgs = NamePieces.size();
llvm::SmallVector<tok::TokenKind, 8> Closes;
std::vector<Range> SelectorPieces;
for (unsigned Index = 0, Last = Tokens.size(); Index < Last - 1; ++Index) {
Expand All @@ -611,12 +614,12 @@ findAllSelectorPieces(llvm::ArrayRef<syntax::Token> Tokens,
auto PieceCount = SelectorPieces.size();
if (PieceCount < NumArgs &&
isMatchingSelectorName(Tok, Tokens[Index + 1], SM,
Sel.getNameForSlot(PieceCount))) {
NamePieces[PieceCount])) {
// If 'foo:' instead of ':' (empty selector), we need to skip the ':'
// token after the name. We don't currently properly support empty
// selectors since we may lex them improperly due to ternary statements
// as well as don't properly support storing their ranges for edits.
if (!Sel.getNameForSlot(PieceCount).empty())
if (!NamePieces[PieceCount].empty())
++Index;
SelectorPieces.push_back(
halfOpenToRange(SM, Tok.range(SM).toCharRange(SM)));
Expand Down Expand Up @@ -668,16 +671,17 @@ findAllSelectorPieces(llvm::ArrayRef<syntax::Token> Tokens,

/// Collects all ranges of the given identifier/selector in the source code.
///
/// If a selector is given, this does a full lex of the given source code in
/// order to identify all selector fragments (e.g. in method exprs/decls) since
/// they are non-contiguous.
std::vector<SymbolRange> collectRenameIdentifierRanges(
llvm::StringRef Identifier, llvm::StringRef Content,
const LangOptions &LangOpts, std::optional<Selector> Selector) {
/// If `Name` is an Objective-C symbol name, this does a full lex of the given
/// source code in order to identify all selector fragments (e.g. in method
/// exprs/decls) since they are non-contiguous.
std::vector<SymbolRange>
collectRenameIdentifierRanges(const RenameSymbolName &Name,
llvm::StringRef Content,
const LangOptions &LangOpts) {
std::vector<SymbolRange> Ranges;
if (!Selector) {
if (auto SinglePiece = Name.getSinglePiece()) {
auto IdentifierRanges =
collectIdentifierRanges(Identifier, Content, LangOpts);
collectIdentifierRanges(*SinglePiece, Content, LangOpts);
for (const auto &R : IdentifierRanges)
Ranges.emplace_back(R);
return Ranges;
Expand All @@ -691,7 +695,7 @@ std::vector<SymbolRange> collectRenameIdentifierRanges(
// parsing a method declaration or definition which isn't at the top level or
// similar looking expressions (e.g. an @selector() expression).
llvm::SmallVector<tok::TokenKind, 8> Closes;
llvm::StringRef FirstSelPiece = Selector->getNameForSlot(0);
llvm::StringRef FirstSelPiece = Name.getNamePieces()[0];

auto Tokens = syntax::tokenize(SM.getMainFileID(), SM, LangOpts);
unsigned Last = Tokens.size() - 1;
Expand Down Expand Up @@ -723,7 +727,7 @@ std::vector<SymbolRange> collectRenameIdentifierRanges(
// Check if we can find all the relevant selector peices starting from
// this token
auto SelectorRanges =
findAllSelectorPieces(ArrayRef(Tokens).slice(Index), SM, *Selector,
findAllSelectorPieces(ArrayRef(Tokens).slice(Index), SM, Name,
Closes.empty() ? tok::l_brace : Closes.back());
if (SelectorRanges)
Ranges.emplace_back(std::move(*SelectorRanges));
Expand Down Expand Up @@ -770,7 +774,6 @@ renameObjCMethodWithinFile(ParsedAST &AST, const ObjCMethodDecl *MD,
std::vector<SourceLocation> SelectorOccurences) {
const SourceManager &SM = AST.getSourceManager();
auto Code = SM.getBufferData(SM.getMainFileID());
auto RenameIdentifier = MD->getSelector().getNameForSlot(0).str();
llvm::SmallVector<llvm::StringRef, 8> NewNames;
NewName.split(NewNames, ":");

Expand All @@ -780,7 +783,7 @@ renameObjCMethodWithinFile(ParsedAST &AST, const ObjCMethodDecl *MD,
Ranges.push_back(tokenRangeForLoc(AST, Loc, SM, LangOpts));
auto FilePath = AST.tuPath();
auto RenameRanges = collectRenameIdentifierRanges(
RenameIdentifier, Code, LangOpts, MD->getSelector());
RenameSymbolName(MD->getDeclName()), Code, LangOpts);
auto RenameEdit = buildRenameEdit(FilePath, Code, RenameRanges, NewNames);
if (!RenameEdit)
return error("failed to rename in file {0}: {1}", FilePath,
Expand Down Expand Up @@ -942,21 +945,14 @@ renameOutsideFile(const NamedDecl &RenameDecl, llvm::StringRef MainFilePath,
ExpBuffer.getError().message());
continue;
}
std::string RenameIdentifier = RenameDecl.getNameAsString();
std::optional<Selector> Selector = std::nullopt;
RenameSymbolName RenameName(RenameDecl.getDeclName());
llvm::SmallVector<llvm::StringRef, 8> NewNames;
if (const auto *MD = dyn_cast<ObjCMethodDecl>(&RenameDecl)) {
RenameIdentifier = MD->getSelector().getNameForSlot(0).str();
if (MD->getSelector().getNumArgs() > 1)
Selector = MD->getSelector();
}
NewName.split(NewNames, ":");

auto AffectedFileCode = (*ExpBuffer)->getBuffer();
auto RenameRanges =
adjustRenameRanges(AffectedFileCode, RenameIdentifier,
std::move(FileAndOccurrences.second),
RenameDecl.getASTContext().getLangOpts(), Selector);
auto RenameRanges = adjustRenameRanges(
AffectedFileCode, RenameName, std::move(FileAndOccurrences.second),
RenameDecl.getASTContext().getLangOpts());
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
Expand Down Expand Up @@ -1017,6 +1013,50 @@ void findNearMiss(

} // namespace

RenameSymbolName::RenameSymbolName() : NamePieces({}) {}

namespace {
std::vector<std::string> extractNamePieces(const DeclarationName &DeclName) {
if (DeclName.getNameKind() ==
DeclarationName::NameKind::ObjCMultiArgSelector ||
DeclName.getNameKind() == DeclarationName::NameKind::ObjCOneArgSelector) {
const Selector &Sel = DeclName.getObjCSelector();
std::vector<std::string> Result;
for (unsigned Slot = 0; Slot < Sel.getNumArgs(); ++Slot) {
Result.push_back(Sel.getNameForSlot(Slot).str());
}
return Result;
}
return {DeclName.getAsString()};
}
} // namespace

RenameSymbolName::RenameSymbolName(const DeclarationName &DeclName)
: RenameSymbolName(extractNamePieces(DeclName)) {}

RenameSymbolName::RenameSymbolName(ArrayRef<std::string> NamePieces) {
for (const auto &Piece : NamePieces)
this->NamePieces.push_back(Piece);
}

std::optional<std::string> RenameSymbolName::getSinglePiece() const {
if (getNamePieces().size() == 1) {
return NamePieces.front();
}
return std::nullopt;
}

std::string RenameSymbolName::getAsString() const {
std::string Result;
llvm::raw_string_ostream OS(Result);
this->print(OS);
return Result;
}

void RenameSymbolName::print(raw_ostream &OS) const {
llvm::interleave(NamePieces, OS, ":");
}

SymbolRange::SymbolRange(Range R) : Ranges({R}) {}

SymbolRange::SymbolRange(std::vector<Range> Ranges)
Expand Down Expand Up @@ -1244,14 +1284,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<SymbolRange>>
adjustRenameRanges(llvm::StringRef DraftCode, llvm::StringRef Identifier,
std::vector<Range> Indexed, const LangOptions &LangOpts,
std::optional<Selector> Selector) {
adjustRenameRanges(llvm::StringRef DraftCode, const RenameSymbolName &Name,
std::vector<Range> Indexed, const LangOptions &LangOpts) {
trace::Span Tracer("AdjustRenameRanges");
assert(!Indexed.empty());
assert(llvm::is_sorted(Indexed));
std::vector<SymbolRange> Lexed =
collectRenameIdentifierRanges(Identifier, DraftCode, LangOpts, Selector);
collectRenameIdentifierRanges(Name, DraftCode, LangOpts);
llvm::sort(Lexed);
return getMappedRanges(Indexed, Lexed);
}
Expand Down
48 changes: 45 additions & 3 deletions clang-tools-extra/clangd/refactor/Rename.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,49 @@ struct RenameOptions {
bool RenameVirtual = true;
};

/// A name of a symbol that should be renamed.
///
/// Symbol's name can be composed of multiple strings. For example, Objective-C
/// methods can contain multiple argument labels:
///
/// \code
/// - (void) myMethodNamePiece: (int)x anotherNamePieces:(int)y;
/// ^~ string 0 ~~~~~ ^~ string 1 ~~~~~
/// \endcode
class RenameSymbolName {
llvm::SmallVector<std::string, 1> NamePieces;

public:
RenameSymbolName();

/// Create a new \c SymbolName with the specified pieces.
explicit RenameSymbolName(ArrayRef<std::string> NamePieces);

explicit RenameSymbolName(const DeclarationName &Name);

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 RenameSymbolName &Other) const {
return NamePieces == Other.NamePieces;
}
};

struct RenameInputs {
Position Pos; // the position triggering the rename
llvm::StringRef NewName;
Expand Down Expand Up @@ -108,9 +151,8 @@ llvm::Expected<Edit> buildRenameEdit(llvm::StringRef AbsFilePath,
/// occurrence has the same length).
/// REQUIRED: Indexed is sorted.
std::optional<std::vector<SymbolRange>>
adjustRenameRanges(llvm::StringRef DraftCode, llvm::StringRef Identifier,
std::vector<Range> Indexed, const LangOptions &LangOpts,
std::optional<Selector> Selector);
adjustRenameRanges(llvm::StringRef DraftCode, const RenameSymbolName &Name,
std::vector<Range> Indexed, const LangOptions &LangOpts);

/// Calculates the lexed occurrences that the given indexed occurrences map to.
/// Returns std::nullopt if we don't find a mapping.
Expand Down
6 changes: 3 additions & 3 deletions clang-tools-extra/clangd/unittests/RenameTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2223,9 +2223,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, std::nullopt);
auto ActualRanges = adjustRenameRanges(
Draft.code(), RenameSymbolName(ArrayRef<std::string>{"x"}),
Annotations(T.IndexedCode).ranges(), LangOpts);
if (!ActualRanges)
EXPECT_THAT(Draft.ranges(), testing::IsEmpty());
else
Expand Down