forked from OSchip/llvm-project
961 lines
37 KiB
C++
961 lines
37 KiB
C++
//===--- 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()) &&
|
|
!isIdentifierHead(Ident.front(), AllowDollar))
|
|
return false;
|
|
for (char C : Ident) {
|
|
if (llvm::isASCII(C) && !isIdentifierBody(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();
|
|
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
|