blob: 76182375ea17047bbc0e622fc239cc3bab09c83a [file] [log] [blame]
//===--- Rename.cpp - Symbol-rename refactorings -----------------*- C++-*-===//
//
// 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 "refactor/Rename.h"
#include "AST.h"
#include "FindTarget.h"
#include "ParsedAST.h"
#include "Selection.h"
#include "SourceCode.h"
#include "index/SymbolCollector.h"
#include "support/Logger.h"
#include "support/Trace.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/ASTTypeTraits.h"
#include "clang/AST/Decl.h"
#include "clang/AST/DeclCXX.h"
#include "clang/AST/DeclTemplate.h"
#include "clang/AST/ParentMapContext.h"
#include "clang/AST/Stmt.h"
#include "clang/Basic/CharInfo.h"
#include "clang/Basic/LLVM.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Tooling/Syntax/Tokens.h"
#include "llvm/ADT/None.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/Support/Casting.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/FormatVariadic.h"
#include "llvm/Support/JSON.h"
#include <algorithm>
namespace clang {
namespace clangd {
namespace {
llvm::Optional<std::string> filePath(const SymbolLocation &Loc,
llvm::StringRef HintFilePath) {
if (!Loc)
return None;
auto Path = URI::resolve(Loc.FileURI, HintFilePath);
if (!Path) {
elog("Could not resolve URI {0}: {1}", Loc.FileURI, Path.takeError());
return None;
}
return *Path;
}
// Returns true if the given location is expanded from any macro body.
bool isInMacroBody(const SourceManager &SM, SourceLocation Loc) {
while (Loc.isMacroID()) {
if (SM.isMacroBodyExpansion(Loc))
return true;
Loc = SM.getImmediateMacroCallerLoc(Loc);
}
return false;
}
// Canonical declarations help simplify the process of renaming. Examples:
// - Template's canonical decl is the templated declaration (i.e.
// ClassTemplateDecl is canonicalized to its child CXXRecordDecl,
// FunctionTemplateDecl - to child FunctionDecl)
// - Given a constructor/destructor, canonical declaration is the parent
// CXXRecordDecl because we want to rename both type name and its ctor/dtor.
// - All specializations are canonicalized to the primary template. For example:
//
// template <typename T, int U>
// bool Foo = true; (1)
//
// template <typename T>
// bool Foo<T, 0> = true; (2)
//
// template <>
// bool Foo<int, 0> = true; (3)
//
// Here, both partial (2) and full (3) specializations are canonicalized to (1)
// which ensures all three of them are renamed.
const NamedDecl *canonicalRenameDecl(const NamedDecl *D) {
if (const auto *VarTemplate = dyn_cast<VarTemplateSpecializationDecl>(D))
return canonicalRenameDecl(
VarTemplate->getSpecializedTemplate()->getTemplatedDecl());
if (const auto *Template = dyn_cast<TemplateDecl>(D))
if (const NamedDecl *TemplatedDecl = Template->getTemplatedDecl())
return canonicalRenameDecl(TemplatedDecl);
if (const auto *ClassTemplateSpecialization =
dyn_cast<ClassTemplateSpecializationDecl>(D))
return canonicalRenameDecl(
ClassTemplateSpecialization->getSpecializedTemplate()
->getTemplatedDecl());
if (const auto *Method = dyn_cast<CXXMethodDecl>(D)) {
if (Method->getDeclKind() == Decl::Kind::CXXConstructor ||
Method->getDeclKind() == Decl::Kind::CXXDestructor)
return canonicalRenameDecl(Method->getParent());
if (const FunctionDecl *InstantiatedMethod =
Method->getInstantiatedFromMemberFunction())
Method = cast<CXXMethodDecl>(InstantiatedMethod);
// FIXME(kirillbobyrev): For virtual methods with
// size_overridden_methods() > 1, this will not rename all functions it
// overrides, because this code assumes there is a single canonical
// declaration.
while (Method->isVirtual() && Method->size_overridden_methods())
Method = *Method->overridden_methods().begin();
return Method->getCanonicalDecl();
}
if (const auto *Function = dyn_cast<FunctionDecl>(D))
if (const FunctionTemplateDecl *Template = Function->getPrimaryTemplate())
return canonicalRenameDecl(Template);
if (const auto *Field = dyn_cast<FieldDecl>(D)) {
// This is a hacky way to do something like
// CXXMethodDecl::getInstantiatedFromMemberFunction for the field because
// Clang AST does not store relevant information about the field that is
// instantiated.
const auto *FieldParent =
dyn_cast_or_null<CXXRecordDecl>(Field->getParent());
if (!FieldParent)
return Field->getCanonicalDecl();
FieldParent = FieldParent->getTemplateInstantiationPattern();
// Field is not instantiation.
if (!FieldParent || Field->getParent() == FieldParent)
return Field->getCanonicalDecl();
for (const FieldDecl *Candidate : FieldParent->fields())
if (Field->getDeclName() == Candidate->getDeclName())
return Candidate->getCanonicalDecl();
elog("FieldParent should have field with the same name as Field.");
}
if (const auto *VD = dyn_cast<VarDecl>(D)) {
if (const VarDecl *OriginalVD = VD->getInstantiatedFromStaticDataMember())
VD = OriginalVD;
return VD->getCanonicalDecl();
}
return dyn_cast<NamedDecl>(D->getCanonicalDecl());
}
llvm::DenseSet<const NamedDecl *> locateDeclAt(ParsedAST &AST,
SourceLocation TokenStartLoc) {
unsigned Offset =
AST.getSourceManager().getDecomposedSpellingLoc(TokenStartLoc).second;
SelectionTree Selection = SelectionTree::createRight(
AST.getASTContext(), AST.getTokens(), Offset, Offset);
const SelectionTree::Node *SelectedNode = Selection.commonAncestor();
if (!SelectedNode)
return {};
llvm::DenseSet<const NamedDecl *> Result;
for (const NamedDecl *D :
targetDecl(SelectedNode->ASTNode,
DeclRelation::Alias | DeclRelation::TemplatePattern,
AST.getHeuristicResolver())) {
Result.insert(canonicalRenameDecl(D));
}
return Result;
}
// By default, we exclude C++ standard symbols and protobuf symbols as rename
// these symbols would change system/generated files which are unlikely to be
// modified.
bool isExcluded(const NamedDecl &RenameDecl) {
if (isProtoFile(RenameDecl.getLocation(),
RenameDecl.getASTContext().getSourceManager()))
return true;
static const auto *StdSymbols = new llvm::DenseSet<llvm::StringRef>({
#define SYMBOL(Name, NameSpace, Header) {#NameSpace #Name},
#include "StdSymbolMap.inc"
#undef SYMBOL
});
return StdSymbols->count(printQualifiedName(RenameDecl));
}
enum class ReasonToReject {
NoSymbolFound,
NoIndexProvided,
NonIndexable,
UnsupportedSymbol,
AmbiguousSymbol,
// name validation. FIXME: reconcile with InvalidName
SameName,
};
llvm::Optional<ReasonToReject> renameable(const NamedDecl &RenameDecl,
StringRef MainFilePath,
const SymbolIndex *Index) {
trace::Span Tracer("Renameable");
// Filter out symbols that are unsupported in both rename modes.
if (llvm::isa<NamespaceDecl>(&RenameDecl))
return ReasonToReject::UnsupportedSymbol;
if (const auto *FD = llvm::dyn_cast<FunctionDecl>(&RenameDecl)) {
if (FD->isOverloadedOperator())
return ReasonToReject::UnsupportedSymbol;
}
// function-local symbols is safe to rename.
if (RenameDecl.getParentFunctionOrMethod())
return None;
if (isExcluded(RenameDecl))
return ReasonToReject::UnsupportedSymbol;
// Check whether the symbol being rename is indexable.
auto &ASTCtx = RenameDecl.getASTContext();
bool MainFileIsHeader = isHeaderFile(MainFilePath, ASTCtx.getLangOpts());
bool DeclaredInMainFile =
isInsideMainFile(RenameDecl.getBeginLoc(), ASTCtx.getSourceManager());
bool IsMainFileOnly = true;
if (MainFileIsHeader)
// main file is a header, the symbol can't be main file only.
IsMainFileOnly = false;
else if (!DeclaredInMainFile)
IsMainFileOnly = false;
// If the symbol is not indexable, we disallow rename.
if (!SymbolCollector::shouldCollectSymbol(
RenameDecl, RenameDecl.getASTContext(), SymbolCollector::Options(),
IsMainFileOnly))
return ReasonToReject::NonIndexable;
// FIXME: Renaming virtual methods requires to rename all overridens in
// subclasses, our index doesn't have this information.
if (const auto *S = llvm::dyn_cast<CXXMethodDecl>(&RenameDecl)) {
if (S->isVirtual())
return ReasonToReject::UnsupportedSymbol;
}
return None;
}
llvm::Error makeError(ReasonToReject Reason) {
auto Message = [](ReasonToReject Reason) {
switch (Reason) {
case ReasonToReject::NoSymbolFound:
return "there is no symbol at the given location";
case ReasonToReject::NoIndexProvided:
return "no index provided";
case ReasonToReject::NonIndexable:
return "symbol may be used in other files (not eligible for indexing)";
case ReasonToReject::UnsupportedSymbol:
return "symbol is not a supported kind (e.g. namespace, macro)";
case ReasonToReject::AmbiguousSymbol:
return "there are multiple symbols at the given location";
case ReasonToReject::SameName:
return "new name is the same as the old name";
}
llvm_unreachable("unhandled reason kind");
};
return error("Cannot rename symbol: {0}", Message(Reason));
}
// Return all rename occurrences in the main file.
std::vector<SourceLocation> findOccurrencesWithinFile(ParsedAST &AST,
const NamedDecl &ND) {
trace::Span Tracer("FindOccurrencesWithinFile");
assert(canonicalRenameDecl(&ND) == &ND &&
"ND should be already canonicalized.");
std::vector<SourceLocation> Results;
for (Decl *TopLevelDecl : AST.getLocalTopLevelDecls()) {
findExplicitReferences(
TopLevelDecl,
[&](ReferenceLoc Ref) {
if (Ref.Targets.empty())
return;
for (const auto *Target : Ref.Targets) {
if (canonicalRenameDecl(Target) == &ND) {
Results.push_back(Ref.NameLoc);
return;
}
}
},
AST.getHeuristicResolver());
}
return Results;
}
// Detect name conflict with othter DeclStmts in the same enclosing scope.
const NamedDecl *lookupSiblingWithinEnclosingScope(ASTContext &Ctx,
const NamedDecl &RenamedDecl,
StringRef NewName) {
// Store Parents list outside of GetSingleParent, so that returned pointer is
// not invalidated.
DynTypedNodeList Storage(DynTypedNode::create(RenamedDecl));
auto GetSingleParent = [&](const DynTypedNode &Node) -> const DynTypedNode * {
Storage = Ctx.getParents(Node);
return (Storage.size() == 1) ? Storage.begin() : nullptr;
};
// We need to get to the enclosing scope: NamedDecl's parent is typically
// DeclStmt (or FunctionProtoTypeLoc in case of function arguments), so
// enclosing scope would be the second order parent.
const auto *Parent = GetSingleParent(DynTypedNode::create(RenamedDecl));
if (!Parent || !(Parent->get<DeclStmt>() || Parent->get<TypeLoc>()))
return nullptr;
Parent = GetSingleParent(*Parent);
// The following helpers check corresponding AST nodes for variable
// declarations with the name collision.
auto CheckDeclStmt = [&](const DeclStmt *DS,
StringRef Name) -> const NamedDecl * {
if (!DS)
return nullptr;
for (const auto &Child : DS->getDeclGroup())
if (const auto *ND = dyn_cast<NamedDecl>(Child))
if (ND != &RenamedDecl && ND->getName() == Name)
return ND;
return nullptr;
};
auto CheckCompoundStmt = [&](const Stmt *S,
StringRef Name) -> const NamedDecl * {
if (const auto *CS = dyn_cast_or_null<CompoundStmt>(S))
for (const auto *Node : CS->children())
if (const auto *Result = CheckDeclStmt(dyn_cast<DeclStmt>(Node), Name))
return Result;
return nullptr;
};
auto CheckConditionVariable = [&](const auto *Scope,
StringRef Name) -> const NamedDecl * {
if (!Scope)
return nullptr;
return CheckDeclStmt(Scope->getConditionVariableDeclStmt(), Name);
};
// CompoundStmt is the most common enclosing scope for function-local symbols
// In the simplest case we just iterate through sibling DeclStmts and check
// for collisions.
if (const auto *EnclosingCS = Parent->get<CompoundStmt>()) {
if (const auto *Result = CheckCompoundStmt(EnclosingCS, NewName))
return Result;
const auto *ScopeParent = GetSingleParent(*Parent);
// CompoundStmt may be found within if/while/for. In these cases, rename can
// collide with the init-statement variable decalaration, they should be
// checked.
if (const auto *Result =
CheckConditionVariable(ScopeParent->get<IfStmt>(), NewName))
return Result;
if (const auto *Result =
CheckConditionVariable(ScopeParent->get<WhileStmt>(), NewName))
return Result;
if (const auto *For = ScopeParent->get<ForStmt>())
if (const auto *Result = CheckDeclStmt(
dyn_cast_or_null<DeclStmt>(For->getInit()), NewName))
return Result;
// Also check if there is a name collision with function arguments.
if (const auto *Function = ScopeParent->get<FunctionDecl>())
for (const auto *Parameter : Function->parameters())
if (Parameter->getName() == NewName)
return Parameter;
return nullptr;
}
// When renaming a variable within init-statement within if/while/for
// condition, also check the CompoundStmt in the body.
if (const auto *EnclosingIf = Parent->get<IfStmt>()) {
if (const auto *Result = CheckCompoundStmt(EnclosingIf->getElse(), NewName))
return Result;
return CheckCompoundStmt(EnclosingIf->getThen(), NewName);
}
if (const auto *EnclosingWhile = Parent->get<WhileStmt>())
return CheckCompoundStmt(EnclosingWhile->getBody(), NewName);
if (const auto *EnclosingFor = Parent->get<ForStmt>()) {
// Check for conflicts with other declarations within initialization
// statement.
if (const auto *Result = CheckDeclStmt(
dyn_cast_or_null<DeclStmt>(EnclosingFor->getInit()), NewName))
return Result;
return CheckCompoundStmt(EnclosingFor->getBody(), NewName);
}
if (const auto *EnclosingFunction = Parent->get<FunctionDecl>()) {
// Check for conflicts with other arguments.
for (const auto *Parameter : EnclosingFunction->parameters())
if (Parameter != &RenamedDecl && Parameter->getName() == NewName)
return Parameter;
// FIXME: We don't modify all references to function parameters when
// renaming from forward declaration now, so using a name colliding with
// something in the definition's body is a valid transformation.
if (!EnclosingFunction->doesThisDeclarationHaveABody())
return nullptr;
return CheckCompoundStmt(EnclosingFunction->getBody(), NewName);
}
return nullptr;
}
// Lookup the declarations (if any) with the given Name in the context of
// RenameDecl.
const NamedDecl *lookupSiblingsWithinContext(ASTContext &Ctx,
const NamedDecl &RenamedDecl,
llvm::StringRef NewName) {
const auto &II = Ctx.Idents.get(NewName);
DeclarationName LookupName(&II);
DeclContextLookupResult LookupResult;
const auto *DC = RenamedDecl.getDeclContext();
while (DC && DC->isTransparentContext())
DC = DC->getParent();
switch (DC->getDeclKind()) {
// The enclosing DeclContext may not be the enclosing scope, it might have
// false positives and negatives, so we only choose "confident" DeclContexts
// that don't have any subscopes that are neither DeclContexts nor
// transparent.
//
// Notably, FunctionDecl is excluded -- because local variables are not scoped
// to the function, but rather to the CompoundStmt that is its body. Lookup
// will not find function-local variables.
case Decl::TranslationUnit:
case Decl::Namespace:
case Decl::Record:
case Decl::Enum:
case Decl::CXXRecord:
LookupResult = DC->lookup(LookupName);
break;
default:
break;
}
// Lookup may contain the RenameDecl itself, exclude it.
for (const auto *D : LookupResult)
if (D->getCanonicalDecl() != RenamedDecl.getCanonicalDecl())
return D;
return nullptr;
}
const NamedDecl *lookupSiblingWithName(ASTContext &Ctx,
const NamedDecl &RenamedDecl,
llvm::StringRef NewName) {
trace::Span Tracer("LookupSiblingWithName");
if (const auto *Result =
lookupSiblingsWithinContext(Ctx, RenamedDecl, NewName))
return Result;
return lookupSiblingWithinEnclosingScope(Ctx, RenamedDecl, NewName);
}
struct InvalidName {
enum Kind {
Keywords,
Conflict,
BadIdentifier,
};
Kind K;
std::string Details;
};
std::string toString(InvalidName::Kind K) {
switch (K) {
case InvalidName::Keywords:
return "Keywords";
case InvalidName::Conflict:
return "Conflict";
case InvalidName::BadIdentifier:
return "BadIdentifier";
}
llvm_unreachable("unhandled InvalidName kind");
}
llvm::Error makeError(InvalidName Reason) {
auto Message = [](InvalidName Reason) {
switch (Reason.K) {
case InvalidName::Keywords:
return llvm::formatv("the chosen name \"{0}\" is a keyword",
Reason.Details);
case InvalidName::Conflict:
return llvm::formatv("conflict with the symbol in {0}", Reason.Details);
case InvalidName::BadIdentifier:
return llvm::formatv("the chosen name \"{0}\" is not a valid identifier",
Reason.Details);
}
llvm_unreachable("unhandled InvalidName kind");
};
return error("invalid name: {0}", Message(Reason));
}
static bool mayBeValidIdentifier(llvm::StringRef Ident) {
assert(llvm::json::isUTF8(Ident));
if (Ident.empty())
return false;
// We don't check all the rules for non-ascii characters (most are allowed).
bool AllowDollar = true; // lenient
if (llvm::isASCII(Ident.front()) &&
!isAsciiIdentifierStart(Ident.front(), AllowDollar))
return false;
for (char C : Ident) {
if (llvm::isASCII(C) && !isAsciiIdentifierContinue(C, AllowDollar))
return false;
}
return true;
}
// Check if we can rename the given RenameDecl into NewName.
// Return details if the rename would produce a conflict.
llvm::Optional<InvalidName> checkName(const NamedDecl &RenameDecl,
llvm::StringRef NewName) {
trace::Span Tracer("CheckName");
static constexpr trace::Metric InvalidNameMetric(
"rename_name_invalid", trace::Metric::Counter, "invalid_kind");
auto &ASTCtx = RenameDecl.getASTContext();
llvm::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) {
if (auto *Conflict = lookupSiblingWithName(ASTCtx, RenameDecl, NewName))
Result = InvalidName{
InvalidName::Conflict,
Conflict->getLocation().printToString(ASTCtx.getSourceManager())};
}
}
if (Result)
InvalidNameMetric.record(1, toString(Result->K));
return Result;
}
// AST-based rename, it renames all occurrences in the main file.
llvm::Expected<tooling::Replacements>
renameWithinFile(ParsedAST &AST, const NamedDecl &RenameDecl,
llvm::StringRef NewName) {
trace::Span Tracer("RenameWithinFile");
const SourceManager &SM = AST.getSourceManager();
tooling::Replacements FilteredChanges;
for (SourceLocation Loc : findOccurrencesWithinFile(AST, RenameDecl)) {
SourceLocation RenameLoc = Loc;
// We don't rename in any macro bodies, but we allow rename the symbol
// spelled in a top-level macro argument in the main file.
if (RenameLoc.isMacroID()) {
if (isInMacroBody(SM, RenameLoc))
continue;
RenameLoc = SM.getSpellingLoc(Loc);
}
// Filter out locations not from main file.
// We traverse only main file decls, but locations could come from an
// non-preamble #include file e.g.
// void test() {
// int f^oo;
// #include "use_foo.inc"
// }
if (!isInsideMainFile(RenameLoc, SM))
continue;
if (auto Err = FilteredChanges.add(tooling::Replacement(
SM, CharSourceRange::getTokenRange(RenameLoc), NewName)))
return std::move(Err);
}
return FilteredChanges;
}
Range toRange(const SymbolLocation &L) {
Range R;
R.start.line = L.Start.line();
R.start.character = L.Start.column();
R.end.line = L.End.line();
R.end.character = L.End.column();
return R;
}
// Return all rename occurrences (using the index) outside of the main file,
// grouped by the absolute file path.
llvm::Expected<llvm::StringMap<std::vector<Range>>>
findOccurrencesOutsideFile(const NamedDecl &RenameDecl,
llvm::StringRef MainFile, const SymbolIndex &Index,
size_t MaxLimitFiles) {
trace::Span Tracer("FindOccurrencesOutsideFile");
RefsRequest RQuest;
RQuest.IDs.insert(getSymbolID(&RenameDecl));
// Absolute file path => rename occurrences in that file.
llvm::StringMap<std::vector<Range>> AffectedFiles;
bool HasMore = Index.refs(RQuest, [&](const Ref &R) {
if (AffectedFiles.size() >= MaxLimitFiles)
return;
if ((R.Kind & RefKind::Spelled) == RefKind::Unknown)
return;
if (auto RefFilePath = filePath(R.Location, /*HintFilePath=*/MainFile)) {
if (!pathEqual(*RefFilePath, MainFile))
AffectedFiles[*RefFilePath].push_back(toRange(R.Location));
}
});
if (AffectedFiles.size() >= MaxLimitFiles)
return error("The number of affected files exceeds the max limit {0}",
MaxLimitFiles);
if (HasMore)
return error("The symbol {0} has too many occurrences",
RenameDecl.getQualifiedNameAsString());
// Sort and deduplicate the results, in case that index returns duplications.
for (auto &FileAndOccurrences : AffectedFiles) {
auto &Ranges = FileAndOccurrences.getValue();
llvm::sort(Ranges);
Ranges.erase(std::unique(Ranges.begin(), Ranges.end()), Ranges.end());
SPAN_ATTACH(Tracer, FileAndOccurrences.first(),
static_cast<int64_t>(Ranges.size()));
}
return AffectedFiles;
}
// Index-based rename, it renames all occurrences outside of the main file.
//
// The cross-file rename is purely based on the index, as we don't want to
// build all ASTs for affected files, which may cause a performance hit.
// We choose to trade off some correctness for performance and scalability.
//
// Clangd builds a dynamic index for all opened files on top of the static
// index of the whole codebase. Dynamic index is up-to-date (respects dirty
// buffers) as long as clangd finishes processing opened files, while static
// index (background index) is relatively stale. We choose the dirty buffers
// as the file content we rename on, and fallback to file content on disk if
// 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) {
trace::Span Tracer("RenameOutsideFile");
auto AffectedFiles = findOccurrencesOutsideFile(RenameDecl, MainFilePath,
Index, MaxLimitFiles);
if (!AffectedFiles)
return AffectedFiles.takeError();
FileEdits Results;
for (auto &FileAndOccurrences : *AffectedFiles) {
llvm::StringRef FilePath = FileAndOccurrences.first();
auto ExpBuffer = FS.getBufferForFile(FilePath);
if (!ExpBuffer) {
elog("Fail to read file content: Fail to open file {0}: {1}", FilePath,
ExpBuffer.getError().message());
continue;
}
auto AffectedFileCode = (*ExpBuffer)->getBuffer();
auto RenameRanges =
adjustRenameRanges(AffectedFileCode, RenameDecl.getNameAsString(),
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
// entire rename.
return error("Index results don't match the content of file {0} "
"(the index may be stale)",
FilePath);
}
auto RenameEdit =
buildRenameEdit(FilePath, AffectedFileCode, *RenameRanges, NewName);
if (!RenameEdit)
return error("failed to rename in file {0}: {1}", FilePath,
RenameEdit.takeError());
if (!RenameEdit->Replacements.empty())
Results.insert({FilePath, std::move(*RenameEdit)});
}
return Results;
}
// A simple edit is either changing line or column, but not both.
bool impliesSimpleEdit(const Position &LHS, const Position &RHS) {
return LHS.line == RHS.line || LHS.character == RHS.character;
}
// Performs a DFS to enumerate all possible near-miss matches.
// It finds the locations where the indexed occurrences are now spelled in
// Lexed occurrences, a near miss is defined as:
// - a near miss maps all of the **name** occurrences from the index onto a
// *subset* of lexed occurrences (we allow a single name refers to more
// than one symbol)
// - all indexed occurrences must be mapped, and Result must be distinct and
// preserve order (only support detecting simple edits to ensure a
// robust mapping)
// - each indexed -> lexed occurrences mapping correspondence may change the
// *line* or *column*, but not both (increases chance of a robust mapping)
void findNearMiss(
std::vector<size_t> &PartialMatch, ArrayRef<Range> IndexedRest,
ArrayRef<Range> LexedRest, int LexedIndex, int &Fuel,
llvm::function_ref<void(const std::vector<size_t> &)> MatchedCB) {
if (--Fuel < 0)
return;
if (IndexedRest.size() > LexedRest.size())
return;
if (IndexedRest.empty()) {
MatchedCB(PartialMatch);
return;
}
if (impliesSimpleEdit(IndexedRest.front().start, LexedRest.front().start)) {
PartialMatch.push_back(LexedIndex);
findNearMiss(PartialMatch, IndexedRest.drop_front(), LexedRest.drop_front(),
LexedIndex + 1, Fuel, MatchedCB);
PartialMatch.pop_back();
}
findNearMiss(PartialMatch, IndexedRest, LexedRest.drop_front(),
LexedIndex + 1, Fuel, MatchedCB);
}
} // namespace
llvm::Expected<RenameResult> rename(const RenameInputs &RInputs) {
assert(!RInputs.Index == !RInputs.FS &&
"Index and FS must either both be specified or both null.");
trace::Span Tracer("Rename flow");
const auto &Opts = RInputs.Opts;
ParsedAST &AST = RInputs.AST;
const SourceManager &SM = AST.getSourceManager();
llvm::StringRef MainFileCode = SM.getBufferData(SM.getMainFileID());
// Try to find the tokens adjacent to the cursor position.
auto Loc = sourceLocationInMainFile(SM, RInputs.Pos);
if (!Loc)
return Loc.takeError();
const syntax::Token *IdentifierToken =
spelledIdentifierTouching(*Loc, AST.getTokens());
// Renames should only triggered on identifiers.
if (!IdentifierToken)
return makeError(ReasonToReject::NoSymbolFound);
Range CurrentIdentifier = halfOpenToRange(
SM, CharSourceRange::getCharRange(IdentifierToken->location(),
IdentifierToken->endLocation()));
// FIXME: Renaming macros is not supported yet, the macro-handling code should
// be moved to rename tooling library.
if (locateMacroAt(*IdentifierToken, AST.getPreprocessor()))
return makeError(ReasonToReject::UnsupportedSymbol);
auto DeclsUnderCursor = locateDeclAt(AST, IdentifierToken->location());
if (DeclsUnderCursor.empty())
return makeError(ReasonToReject::NoSymbolFound);
if (DeclsUnderCursor.size() > 1)
return makeError(ReasonToReject::AmbiguousSymbol);
const auto &RenameDecl = **DeclsUnderCursor.begin();
const auto *ID = RenameDecl.getIdentifier();
if (!ID)
return makeError(ReasonToReject::UnsupportedSymbol);
if (ID->getName() == RInputs.NewName)
return makeError(ReasonToReject::SameName);
auto Invalid = checkName(RenameDecl, RInputs.NewName);
if (Invalid)
return makeError(*Invalid);
auto Reject = renameable(RenameDecl, RInputs.MainFilePath, RInputs.Index);
if (Reject)
return makeError(*Reject);
// We have two implementations of the rename:
// - AST-based rename: used for renaming local symbols, e.g. variables
// defined in a function body;
// - index-based rename: used for renaming non-local symbols, and not
// feasible for local symbols (as by design our index don't index these
// symbols by design;
// 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);
if (!MainFileRenameEdit)
return MainFileRenameEdit.takeError();
// Check the rename-triggering location is actually being renamed.
// This is a robustness check to avoid surprising rename results -- if the
// the triggering location is not actually the name of the node we identified
// (e.g. for broken code), then rename is likely not what users expect, so we
// reject this kind of rename.
auto StartOffset = positionToOffset(MainFileCode, CurrentIdentifier.start);
auto EndOffset = positionToOffset(MainFileCode, CurrentIdentifier.end);
if (!StartOffset)
return StartOffset.takeError();
if (!EndOffset)
return EndOffset.takeError();
if (llvm::find_if(
*MainFileRenameEdit,
[&StartOffset, &EndOffset](const clang::tooling::Replacement &R) {
return R.getOffset() == *StartOffset &&
R.getLength() == *EndOffset - *StartOffset;
}) == MainFileRenameEdit->end()) {
return makeError(ReasonToReject::NoSymbolFound);
}
RenameResult Result;
Result.Target = CurrentIdentifier;
Edit MainFileEdits = Edit(MainFileCode, std::move(*MainFileRenameEdit));
llvm::for_each(MainFileEdits.asTextEdits(), [&Result](const TextEdit &TE) {
Result.LocalChanges.push_back(TE.range);
});
// return the main file edit if this is a within-file rename or the symbol
// being renamed is function local.
if (RenameDecl.getParentFunctionOrMethod()) {
Result.GlobalChanges = FileEdits(
{std::make_pair(RInputs.MainFilePath, std::move(MainFileEdits))});
return Result;
}
// If the index is nullptr, we don't know the completeness of the result, so
// we don't populate the field GlobalChanges.
if (!RInputs.Index) {
assert(Result.GlobalChanges.empty());
return Result;
}
auto OtherFilesEdits = renameOutsideFile(
RenameDecl, RInputs.MainFilePath, RInputs.NewName, *RInputs.Index,
Opts.LimitFiles == 0 ? std::numeric_limits<size_t>::max()
: Opts.LimitFiles,
*RInputs.FS);
if (!OtherFilesEdits)
return OtherFilesEdits.takeError();
Result.GlobalChanges = *OtherFilesEdits;
// Attach the rename edits for the main file.
Result.GlobalChanges.try_emplace(RInputs.MainFilePath,
std::move(MainFileEdits));
return Result;
}
llvm::Expected<Edit> buildRenameEdit(llvm::StringRef AbsFilePath,
llvm::StringRef InitialCode,
std::vector<Range> Occurrences,
llvm::StringRef NewName) {
trace::Span Tracer("BuildRenameEdit");
SPAN_ATTACH(Tracer, "file_path", AbsFilePath);
SPAN_ATTACH(Tracer, "rename_occurrences",
static_cast<int64_t>(Occurrences.size()));
assert(std::is_sorted(Occurrences.begin(), Occurrences.end()));
assert(std::unique(Occurrences.begin(), Occurrences.end()) ==
Occurrences.end() &&
"Occurrences must be unique");
// These two always correspond to the same position.
Position LastPos{0, 0};
size_t LastOffset = 0;
auto Offset = [&](const Position &P) -> llvm::Expected<size_t> {
assert(LastPos <= P && "malformed input");
Position Shifted = {
P.line - LastPos.line,
P.line > LastPos.line ? P.character : P.character - LastPos.character};
auto ShiftedOffset =
positionToOffset(InitialCode.substr(LastOffset), Shifted);
if (!ShiftedOffset)
return error("fail to convert the position {0} to offset ({1})", P,
ShiftedOffset.takeError());
LastPos = P;
LastOffset += *ShiftedOffset;
return LastOffset;
};
std::vector<std::pair</*start*/ size_t, /*end*/ size_t>> OccurrencesOffsets;
for (const auto &R : Occurrences) {
auto StartOffset = Offset(R.start);
if (!StartOffset)
return StartOffset.takeError();
auto EndOffset = Offset(R.end);
if (!EndOffset)
return EndOffset.takeError();
OccurrencesOffsets.push_back({*StartOffset, *EndOffset});
}
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);
}
return Edit(InitialCode, std::move(RenameEdit));
}
// Details:
// - lex the draft code to get all rename candidates, this yields a superset
// of candidates.
// - apply range patching heuristics to generate "authoritative" occurrences,
// cases we consider:
// (a) index returns a subset of candidates, we use the indexed results.
// - fully equal, we are sure the index is up-to-date
// - proper subset, index is correct in most cases? there may be false
// positives (e.g. candidates got appended), but rename is still safe
// (b) index returns non-candidate results, we attempt to map the indexed
// ranges onto candidates in a plausible way (e.g. guess that lines
// were inserted). If such a "near miss" is found, the rename is still
// possible
llvm::Optional<std::vector<Range>>
adjustRenameRanges(llvm::StringRef DraftCode, llvm::StringRef Identifier,
std::vector<Range> Indexed, const LangOptions &LangOpts) {
trace::Span Tracer("AdjustRenameRanges");
assert(!Indexed.empty());
assert(std::is_sorted(Indexed.begin(), Indexed.end()));
std::vector<Range> Lexed =
collectIdentifierRanges(Identifier, DraftCode, LangOpts);
llvm::sort(Lexed);
return getMappedRanges(Indexed, Lexed);
}
llvm::Optional<std::vector<Range>> getMappedRanges(ArrayRef<Range> Indexed,
ArrayRef<Range> Lexed) {
trace::Span Tracer("GetMappedRanges");
assert(!Indexed.empty());
assert(std::is_sorted(Indexed.begin(), Indexed.end()));
assert(std::is_sorted(Lexed.begin(), Lexed.end()));
if (Indexed.size() > Lexed.size()) {
vlog("The number of lexed occurrences is less than indexed occurrences");
SPAN_ATTACH(
Tracer, "error",
"The number of lexed occurrences is less than indexed occurrences");
return llvm::None;
}
// Fast check for the special subset case.
if (std::includes(Indexed.begin(), Indexed.end(), Lexed.begin(), Lexed.end()))
return Indexed.vec();
std::vector<size_t> Best;
size_t BestCost = std::numeric_limits<size_t>::max();
bool HasMultiple = 0;
std::vector<size_t> ResultStorage;
int Fuel = 10000;
findNearMiss(ResultStorage, Indexed, Lexed, 0, Fuel,
[&](const std::vector<size_t> &Matched) {
size_t MCost =
renameRangeAdjustmentCost(Indexed, Lexed, Matched);
if (MCost < BestCost) {
BestCost = MCost;
Best = std::move(Matched);
HasMultiple = false; // reset
return;
}
if (MCost == BestCost)
HasMultiple = true;
});
if (HasMultiple) {
vlog("The best near miss is not unique.");
SPAN_ATTACH(Tracer, "error", "The best near miss is not unique");
return llvm::None;
}
if (Best.empty()) {
vlog("Didn't find a near miss.");
SPAN_ATTACH(Tracer, "error", "Didn't find a near miss");
return llvm::None;
}
std::vector<Range> Mapped;
for (auto I : Best)
Mapped.push_back(Lexed[I]);
SPAN_ATTACH(Tracer, "mapped_ranges", static_cast<int64_t>(Mapped.size()));
return Mapped;
}
// The cost is the sum of the implied edit sizes between successive diffs, only
// simple edits are considered:
// - insert/remove a line (change line offset)
// - insert/remove a character on an existing line (change column offset)
//
// Example I, total result is 1 + 1 = 2.
// diff[0]: line + 1 <- insert a line before edit 0.
// diff[1]: line + 1
// diff[2]: line + 1
// diff[3]: line + 2 <- insert a line before edits 2 and 3.
//
// Example II, total result is 1 + 1 + 1 = 3.
// diff[0]: line + 1 <- insert a line before edit 0.
// diff[1]: column + 1 <- remove a line between edits 0 and 1, and insert a
// character on edit 1.
size_t renameRangeAdjustmentCost(ArrayRef<Range> Indexed, ArrayRef<Range> Lexed,
ArrayRef<size_t> MappedIndex) {
assert(Indexed.size() == MappedIndex.size());
assert(std::is_sorted(Indexed.begin(), Indexed.end()));
assert(std::is_sorted(Lexed.begin(), Lexed.end()));
int LastLine = -1;
int LastDLine = 0, LastDColumn = 0;
int Cost = 0;
for (size_t I = 0; I < Indexed.size(); ++I) {
int DLine = Indexed[I].start.line - Lexed[MappedIndex[I]].start.line;
int DColumn =
Indexed[I].start.character - Lexed[MappedIndex[I]].start.character;
int Line = Indexed[I].start.line;
if (Line != LastLine)
LastDColumn = 0; // column offsets don't carry cross lines.
Cost += abs(DLine - LastDLine) + abs(DColumn - LastDColumn);
std::tie(LastLine, LastDLine, LastDColumn) = std::tie(Line, DLine, DColumn);
}
return Cost;
}
} // namespace clangd
} // namespace clang