| //===--- 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 "Logger.h" |
| #include "ParsedAST.h" |
| #include "SourceCode.h" |
| #include "index/SymbolCollector.h" |
| #include "clang/Tooling/Refactoring/Rename/RenamingAction.h" |
| #include "clang/Tooling/Refactoring/Rename/USRFinder.h" |
| #include "clang/Tooling/Refactoring/Rename/USRFindingAction.h" |
| #include "clang/Tooling/Refactoring/Rename/USRLocFinder.h" |
| |
| namespace clang { |
| namespace clangd { |
| namespace { |
| |
| llvm::Optional<std::string> filePath(const SymbolLocation &Loc, |
| llvm::StringRef HintFilePath) { |
| if (!Loc) |
| return None; |
| auto Uri = URI::parse(Loc.FileURI); |
| if (!Uri) { |
| elog("Could not parse URI {0}: {1}", Loc.FileURI, Uri.takeError()); |
| return None; |
| } |
| auto U = URIForFile::fromURI(*Uri, HintFilePath); |
| if (!U) { |
| elog("Could not resolve URI {0}: {1}", Loc.FileURI, U.takeError()); |
| return None; |
| } |
| return U->file().str(); |
| } |
| |
| // Query the index to find some other files where the Decl is referenced. |
| llvm::Optional<std::string> getOtherRefFile(const Decl &D, StringRef MainFile, |
| const SymbolIndex &Index) { |
| RefsRequest Req; |
| // We limit the number of results, this is a correctness/performance |
| // tradeoff. We expect the number of symbol references in the current file |
| // is smaller than the limit. |
| Req.Limit = 100; |
| if (auto ID = getSymbolID(&D)) |
| Req.IDs.insert(*ID); |
| llvm::Optional<std::string> OtherFile; |
| Index.refs(Req, [&](const Ref &R) { |
| if (OtherFile) |
| return; |
| if (auto RefFilePath = filePath(R.Location, /*HintFilePath=*/MainFile)) { |
| if (*RefFilePath != MainFile) |
| OtherFile = *RefFilePath; |
| } |
| }); |
| return OtherFile; |
| } |
| |
| enum ReasonToReject { |
| NoSymbolFound, |
| NoIndexProvided, |
| NonIndexable, |
| UsedOutsideFile, |
| UnsupportedSymbol, |
| }; |
| |
| // Check the symbol Decl is renameable (per the index) within the file. |
| llvm::Optional<ReasonToReject> renamableWithinFile(const Decl &RenameDecl, |
| StringRef MainFile, |
| const SymbolIndex *Index) { |
| if (llvm::isa<NamespaceDecl>(&RenameDecl)) |
| return ReasonToReject::UnsupportedSymbol; |
| auto &ASTCtx = RenameDecl.getASTContext(); |
| const auto &SM = ASTCtx.getSourceManager(); |
| bool MainFileIsHeader = ASTCtx.getLangOpts().IsHeaderFile; |
| bool DeclaredInMainFile = isInsideMainFile(RenameDecl.getBeginLoc(), SM); |
| |
| // If the symbol is declared in the main file (which is not a header), we |
| // rename it. |
| if (DeclaredInMainFile && !MainFileIsHeader) |
| return None; |
| |
| // Below are cases where the symbol is declared in the header. |
| // If the symbol is function-local, we rename it. |
| if (RenameDecl.getParentFunctionOrMethod()) |
| return None; |
| |
| if (!Index) |
| return ReasonToReject::NoIndexProvided; |
| |
| bool IsIndexable = isa<NamedDecl>(RenameDecl) && |
| SymbolCollector::shouldCollectSymbol( |
| cast<NamedDecl>(RenameDecl), ASTCtx, {}, false); |
| // If the symbol is not indexable, we disallow rename. |
| if (!IsIndexable) |
| return ReasonToReject::NonIndexable; |
| auto OtherFile = getOtherRefFile(RenameDecl, MainFile, *Index); |
| // If the symbol is indexable and has no refs from other files in the index, |
| // we rename it. |
| if (!OtherFile) |
| return None; |
| // If the symbol is indexable and has refs from other files in the index, |
| // we disallow rename. |
| return ReasonToReject::UsedOutsideFile; |
| } |
| |
| llvm::Error makeError(ReasonToReject Reason) { |
| auto Message = [](ReasonToReject Reason) { |
| switch (Reason) { |
| case NoSymbolFound: |
| return "there is no symbol at the given location"; |
| case NoIndexProvided: |
| return "symbol may be used in other files (no index available)"; |
| case UsedOutsideFile: |
| return "the symbol is used outside main file"; |
| case NonIndexable: |
| return "symbol may be used in other files (not eligible for indexing)"; |
| case UnsupportedSymbol: |
| return "symbol is not a supported kind (e.g. namespace, macro)"; |
| } |
| llvm_unreachable("unhandled reason kind"); |
| }; |
| return llvm::make_error<llvm::StringError>( |
| llvm::formatv("Cannot rename symbol: {0}", Message(Reason)), |
| llvm::inconvertibleErrorCode()); |
| } |
| |
| // Return all rename occurrences in the main file. |
| tooling::SymbolOccurrences |
| findOccurrencesWithinFile(ParsedAST &AST, const NamedDecl *RenameDecl) { |
| const NamedDecl *CanonicalRenameDecl = |
| tooling::getCanonicalSymbolDeclaration(RenameDecl); |
| assert(CanonicalRenameDecl && "RenameDecl must be not null"); |
| std::vector<std::string> RenameUSRs = |
| tooling::getUSRsForDeclaration(CanonicalRenameDecl, AST.getASTContext()); |
| std::string OldName = CanonicalRenameDecl->getNameAsString(); |
| tooling::SymbolOccurrences Result; |
| for (Decl *TopLevelDecl : AST.getLocalTopLevelDecls()) { |
| tooling::SymbolOccurrences RenameInDecl = |
| tooling::getOccurrencesOfUSRs(RenameUSRs, OldName, TopLevelDecl); |
| Result.insert(Result.end(), std::make_move_iterator(RenameInDecl.begin()), |
| std::make_move_iterator(RenameInDecl.end())); |
| } |
| return Result; |
| } |
| |
| } // namespace |
| |
| llvm::Expected<tooling::Replacements> |
| renameWithinFile(ParsedAST &AST, llvm::StringRef File, Position Pos, |
| llvm::StringRef NewName, const SymbolIndex *Index) { |
| const SourceManager &SM = AST.getSourceManager(); |
| SourceLocation SourceLocationBeg = SM.getMacroArgExpandedLocation( |
| getBeginningOfIdentifier(Pos, SM, AST.getASTContext().getLangOpts())); |
| // FIXME: renaming macros is not supported yet, the macro-handling code should |
| // be moved to rename tooling library. |
| if (locateMacroAt(SourceLocationBeg, AST.getPreprocessor())) |
| return makeError(UnsupportedSymbol); |
| |
| const auto *RenameDecl = |
| tooling::getNamedDeclAt(AST.getASTContext(), SourceLocationBeg); |
| if (!RenameDecl) |
| return makeError(NoSymbolFound); |
| |
| if (auto Reject = |
| renamableWithinFile(*RenameDecl->getCanonicalDecl(), File, Index)) |
| return makeError(*Reject); |
| |
| // Rename sometimes returns duplicate edits (which is a bug). A side-effect of |
| // adding them to a single Replacements object is these are deduplicated. |
| tooling::Replacements FilteredChanges; |
| for (const tooling::SymbolOccurrence &Rename : |
| findOccurrencesWithinFile(AST, RenameDecl)) { |
| // Currently, we only support normal rename (one range) for C/C++. |
| // FIXME: support multiple-range rename for objective-c methods. |
| if (Rename.getNameRanges().size() > 1) |
| continue; |
| // We shouldn't have conflicting replacements. If there are conflicts, it |
| // means that we have bugs either in clangd or in Rename library, therefore |
| // we refuse to perform the rename. |
| if (auto Err = FilteredChanges.add(tooling::Replacement( |
| AST.getASTContext().getSourceManager(), |
| CharSourceRange::getCharRange(Rename.getNameRanges()[0]), NewName))) |
| return std::move(Err); |
| } |
| return FilteredChanges; |
| } |
| |
| } // namespace clangd |
| } // namespace clang |