| //===--- 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 |