//===--- 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 namespace clang { namespace clangd { namespace { llvm::Optional 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 // bool Foo = true; (1) // // template // bool Foo = true; (2) // // template <> // bool Foo = 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(D)) return canonicalRenameDecl( VarTemplate->getSpecializedTemplate()->getTemplatedDecl()); if (const auto *Template = dyn_cast(D)) if (const NamedDecl *TemplatedDecl = Template->getTemplatedDecl()) return canonicalRenameDecl(TemplatedDecl); if (const auto *ClassTemplateSpecialization = dyn_cast(D)) return canonicalRenameDecl( ClassTemplateSpecialization->getSpecializedTemplate() ->getTemplatedDecl()); if (const auto *Method = dyn_cast(D)) { if (Method->getDeclKind() == Decl::Kind::CXXConstructor || Method->getDeclKind() == Decl::Kind::CXXDestructor) return canonicalRenameDecl(Method->getParent()); if (const FunctionDecl *InstantiatedMethod = Method->getInstantiatedFromMemberFunction()) Method = cast(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(D)) if (const FunctionTemplateDecl *Template = Function->getPrimaryTemplate()) return canonicalRenameDecl(Template); if (const auto *Field = dyn_cast(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(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(D)) { if (const VarDecl *OriginalVD = VD->getInstantiatedFromStaticDataMember()) VD = OriginalVD; return VD->getCanonicalDecl(); } return dyn_cast(D->getCanonicalDecl()); } llvm::DenseSet 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 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({ #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 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(&RenameDecl)) return ReasonToReject::UnsupportedSymbol; if (const auto *FD = llvm::dyn_cast(&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(&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 findOccurrencesWithinFile(ParsedAST &AST, const NamedDecl &ND) { trace::Span Tracer("FindOccurrencesWithinFile"); assert(canonicalRenameDecl(&ND) == &ND && "ND should be already canonicalized."); std::vector 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() || Parent->get())) 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(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(S)) for (const auto *Node : CS->children()) if (const auto *Result = CheckDeclStmt(dyn_cast(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()) { 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(), NewName)) return Result; if (const auto *Result = CheckConditionVariable(ScopeParent->get(), NewName)) return Result; if (const auto *For = ScopeParent->get()) if (const auto *Result = CheckDeclStmt( dyn_cast_or_null(For->getInit()), NewName)) return Result; // Also check if there is a name collision with function arguments. if (const auto *Function = ScopeParent->get()) 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()) { if (const auto *Result = CheckCompoundStmt(EnclosingIf->getElse(), NewName)) return Result; return CheckCompoundStmt(EnclosingIf->getThen(), NewName); } if (const auto *EnclosingWhile = Parent->get()) return CheckCompoundStmt(EnclosingWhile->getBody(), NewName); if (const auto *EnclosingFor = Parent->get()) { // Check for conflicts with other declarations within initialization // statement. if (const auto *Result = CheckDeclStmt( dyn_cast_or_null(EnclosingFor->getInit()), NewName)) return Result; return CheckCompoundStmt(EnclosingFor->getBody(), NewName); } if (const auto *EnclosingFunction = Parent->get()) { // 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 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 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 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>> 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> 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(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 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 &PartialMatch, ArrayRef IndexedRest, ArrayRef LexedRest, int LexedIndex, int &Fuel, llvm::function_ref &)> 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 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::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 buildRenameEdit(llvm::StringRef AbsFilePath, llvm::StringRef InitialCode, std::vector Occurrences, llvm::StringRef NewName) { trace::Span Tracer("BuildRenameEdit"); SPAN_ATTACH(Tracer, "file_path", AbsFilePath); SPAN_ATTACH(Tracer, "rename_occurrences", static_cast(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 { 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> 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> adjustRenameRanges(llvm::StringRef DraftCode, llvm::StringRef Identifier, std::vector Indexed, const LangOptions &LangOpts) { trace::Span Tracer("AdjustRenameRanges"); assert(!Indexed.empty()); assert(std::is_sorted(Indexed.begin(), Indexed.end())); std::vector Lexed = collectIdentifierRanges(Identifier, DraftCode, LangOpts); llvm::sort(Lexed); return getMappedRanges(Indexed, Lexed); } llvm::Optional> getMappedRanges(ArrayRef Indexed, ArrayRef 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 Best; size_t BestCost = std::numeric_limits::max(); bool HasMultiple = 0; std::vector ResultStorage; int Fuel = 10000; findNearMiss(ResultStorage, Indexed, Lexed, 0, Fuel, [&](const std::vector &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 Mapped; for (auto I : Best) Mapped.push_back(Lexed[I]); SPAN_ATTACH(Tracer, "mapped_ranges", static_cast(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 Indexed, ArrayRef Lexed, ArrayRef 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