blob: 44ebdbce628d4841c8ba0e10d4345cba3314c881 [file] [log] [blame]
Haojian Wu488c5092019-05-31 14:38:16 +00001//===--- Rename.cpp - Symbol-rename refactorings -----------------*- C++-*-===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8
Sam McCallc0949122019-05-07 07:11:56 +00009#include "refactor/Rename.h"
Haojian Wu7276a442019-06-25 08:43:17 +000010#include "AST.h"
Haojian Wu7db12302019-11-19 10:10:43 +010011#include "FindTarget.h"
Haojian Wu7276a442019-06-25 08:43:17 +000012#include "Logger.h"
Sam McCall915f9782019-09-04 09:46:06 +000013#include "ParsedAST.h"
Haojian Wu7db12302019-11-19 10:10:43 +010014#include "Selection.h"
Sam McCall19cefc22019-09-03 15:34:47 +000015#include "SourceCode.h"
Haojian Wu7276a442019-06-25 08:43:17 +000016#include "index/SymbolCollector.h"
Haojian Wu7db12302019-11-19 10:10:43 +010017#include "clang/AST/DeclCXX.h"
18#include "clang/AST/DeclTemplate.h"
19#include "clang/Basic/SourceLocation.h"
Haojian Wu8b491732019-08-09 10:55:22 +000020#include "clang/Tooling/Refactoring/Rename/USRFindingAction.h"
Kirill Bobyrevec618822019-12-12 13:10:59 +010021#include "clang/Tooling/Syntax/Tokens.h"
Haojian Wu891f8222019-12-09 17:00:51 +010022#include "llvm/ADT/None.h"
Haojian Wu852bafa2019-10-23 14:40:20 +020023#include "llvm/ADT/STLExtras.h"
Kirill Bobyrevec618822019-12-12 13:10:59 +010024#include "llvm/Support/Casting.h"
Haojian Wu852bafa2019-10-23 14:40:20 +020025#include "llvm/Support/Error.h"
Haojian Wu88053162019-11-19 15:23:36 +010026#include "llvm/Support/FormatVariadic.h"
Haojian Wu891f8222019-12-09 17:00:51 +010027#include <algorithm>
Sam McCallc0949122019-05-07 07:11:56 +000028
29namespace clang {
30namespace clangd {
31namespace {
32
Haojian Wu7276a442019-06-25 08:43:17 +000033llvm::Optional<std::string> filePath(const SymbolLocation &Loc,
34 llvm::StringRef HintFilePath) {
35 if (!Loc)
36 return None;
Haojian Wuf821ab82019-10-29 10:49:27 +010037 auto Path = URI::resolve(Loc.FileURI, HintFilePath);
38 if (!Path) {
39 elog("Could not resolve URI {0}: {1}", Loc.FileURI, Path.takeError());
Haojian Wu7276a442019-06-25 08:43:17 +000040 return None;
41 }
Haojian Wuf821ab82019-10-29 10:49:27 +010042
43 return *Path;
Haojian Wu7276a442019-06-25 08:43:17 +000044}
45
Haojian Wu7db12302019-11-19 10:10:43 +010046// Returns true if the given location is expanded from any macro body.
47bool isInMacroBody(const SourceManager &SM, SourceLocation Loc) {
48 while (Loc.isMacroID()) {
49 if (SM.isMacroBodyExpansion(Loc))
50 return true;
51 Loc = SM.getImmediateMacroCallerLoc(Loc);
52 }
53
54 return false;
55}
56
Haojian Wu7276a442019-06-25 08:43:17 +000057// Query the index to find some other files where the Decl is referenced.
Haojian Wu93a825c2019-06-27 13:24:10 +000058llvm::Optional<std::string> getOtherRefFile(const Decl &D, StringRef MainFile,
Haojian Wu7276a442019-06-25 08:43:17 +000059 const SymbolIndex &Index) {
60 RefsRequest Req;
61 // We limit the number of results, this is a correctness/performance
62 // tradeoff. We expect the number of symbol references in the current file
63 // is smaller than the limit.
64 Req.Limit = 100;
Haojian Wu852bafa2019-10-23 14:40:20 +020065 Req.IDs.insert(*getSymbolID(&D));
Haojian Wu7276a442019-06-25 08:43:17 +000066 llvm::Optional<std::string> OtherFile;
67 Index.refs(Req, [&](const Ref &R) {
68 if (OtherFile)
69 return;
70 if (auto RefFilePath = filePath(R.Location, /*HintFilePath=*/MainFile)) {
71 if (*RefFilePath != MainFile)
72 OtherFile = *RefFilePath;
73 }
74 });
75 return OtherFile;
76}
77
Haojian Wu0d893fd2020-01-27 10:39:55 +010078llvm::DenseSet<const NamedDecl *> locateDeclAt(ParsedAST &AST,
79 SourceLocation TokenStartLoc) {
Haojian Wu7db12302019-11-19 10:10:43 +010080 unsigned Offset =
81 AST.getSourceManager().getDecomposedSpellingLoc(TokenStartLoc).second;
82
Sam McCall6af1ad22019-12-16 19:07:49 +010083 SelectionTree Selection(AST.getASTContext(), AST.getTokens(), Offset);
Haojian Wu7db12302019-11-19 10:10:43 +010084 const SelectionTree::Node *SelectedNode = Selection.commonAncestor();
85 if (!SelectedNode)
86 return {};
87
Haojian Wu0d893fd2020-01-27 10:39:55 +010088 llvm::DenseSet<const NamedDecl *> Result;
Sam McCallf06f4392020-01-03 17:26:33 +010089 for (const NamedDecl *D :
Haojian Wu7db12302019-11-19 10:10:43 +010090 targetDecl(SelectedNode->ASTNode,
Kirill Bobyrevec618822019-12-12 13:10:59 +010091 DeclRelation::Alias | DeclRelation::TemplatePattern)) {
Kirill Bobyrevec618822019-12-12 13:10:59 +010092 // Get to CXXRecordDecl from constructor or destructor.
Sam McCallf06f4392020-01-03 17:26:33 +010093 D = tooling::getCanonicalSymbolDeclaration(D);
94 Result.insert(D);
Kirill Bobyrevec618822019-12-12 13:10:59 +010095 }
Haojian Wu7db12302019-11-19 10:10:43 +010096 return Result;
97}
98
Haojian Wud6da8a12020-02-05 12:31:11 +010099// By default, we blacklist C++ standard symbols and protobuf symbols as rename
100// these symbols would change system/generated files which are unlikely to be
101// modified.
Haojian Wu0d893fd2020-01-27 10:39:55 +0100102bool isBlacklisted(const NamedDecl &RenameDecl) {
Haojian Wud6da8a12020-02-05 12:31:11 +0100103 if (isProtoFile(RenameDecl.getLocation(),
104 RenameDecl.getASTContext().getSourceManager()))
105 return true;
Haojian Wu0d893fd2020-01-27 10:39:55 +0100106 static const auto *StdSymbols = new llvm::DenseSet<llvm::StringRef>({
107#define SYMBOL(Name, NameSpace, Header) {#NameSpace #Name},
108#include "StdSymbolMap.inc"
109#undef SYMBOL
110 });
111 return StdSymbols->count(RenameDecl.getQualifiedNameAsString());
112}
113
Haojian Wu7276a442019-06-25 08:43:17 +0000114enum ReasonToReject {
Haojian Wu8b491732019-08-09 10:55:22 +0000115 NoSymbolFound,
Haojian Wu7276a442019-06-25 08:43:17 +0000116 NoIndexProvided,
117 NonIndexable,
Haojian Wu852bafa2019-10-23 14:40:20 +0200118 UsedOutsideFile, // for within-file rename only.
Haojian Wu442a1202019-06-26 08:10:26 +0000119 UnsupportedSymbol,
Haojian Wu7db12302019-11-19 10:10:43 +0100120 AmbiguousSymbol,
Haojian Wu7276a442019-06-25 08:43:17 +0000121};
122
Haojian Wu0d893fd2020-01-27 10:39:55 +0100123llvm::Optional<ReasonToReject> renameable(const NamedDecl &RenameDecl,
Haojian Wu852bafa2019-10-23 14:40:20 +0200124 StringRef MainFilePath,
125 const SymbolIndex *Index,
126 bool CrossFile) {
127 // Filter out symbols that are unsupported in both rename modes.
Haojian Wu93a825c2019-06-27 13:24:10 +0000128 if (llvm::isa<NamespaceDecl>(&RenameDecl))
Haojian Wu442a1202019-06-26 08:10:26 +0000129 return ReasonToReject::UnsupportedSymbol;
Haojian Wuaf28bb62019-09-16 10:16:56 +0000130 if (const auto *FD = llvm::dyn_cast<FunctionDecl>(&RenameDecl)) {
131 if (FD->isOverloadedOperator())
132 return ReasonToReject::UnsupportedSymbol;
133 }
Haojian Wu852bafa2019-10-23 14:40:20 +0200134 // function-local symbols is safe to rename.
Haojian Wu93a825c2019-06-27 13:24:10 +0000135 if (RenameDecl.getParentFunctionOrMethod())
Haojian Wu7276a442019-06-25 08:43:17 +0000136 return None;
137
Haojian Wu0d893fd2020-01-27 10:39:55 +0100138 if (isBlacklisted(RenameDecl))
139 return ReasonToReject::UnsupportedSymbol;
140
Haojian Wu902dc6c2019-11-29 14:58:44 +0100141 // Check whether the symbol being rename is indexable.
142 auto &ASTCtx = RenameDecl.getASTContext();
143 bool MainFileIsHeader = isHeaderFile(MainFilePath, ASTCtx.getLangOpts());
144 bool DeclaredInMainFile =
145 isInsideMainFile(RenameDecl.getBeginLoc(), ASTCtx.getSourceManager());
146 bool IsMainFileOnly = true;
147 if (MainFileIsHeader)
148 // main file is a header, the symbol can't be main file only.
149 IsMainFileOnly = false;
150 else if (!DeclaredInMainFile)
151 IsMainFileOnly = false;
Haojian Wu0d893fd2020-01-27 10:39:55 +0100152 // If the symbol is not indexable, we disallow rename.
153 if (!SymbolCollector::shouldCollectSymbol(
154 RenameDecl, RenameDecl.getASTContext(), SymbolCollector::Options(),
155 IsMainFileOnly))
Haojian Wu852bafa2019-10-23 14:40:20 +0200156 return ReasonToReject::NonIndexable;
157
158 if (!CrossFile) {
Haojian Wu852bafa2019-10-23 14:40:20 +0200159 if (!DeclaredInMainFile)
160 // We are sure the symbol is used externally, bail out early.
161 return ReasonToReject::UsedOutsideFile;
162
163 // If the symbol is declared in the main file (which is not a header), we
164 // rename it.
165 if (!MainFileIsHeader)
166 return None;
167
168 if (!Index)
169 return ReasonToReject::NoIndexProvided;
170
171 auto OtherFile = getOtherRefFile(RenameDecl, MainFilePath, *Index);
172 // If the symbol is indexable and has no refs from other files in the index,
173 // we rename it.
174 if (!OtherFile)
175 return None;
176 // If the symbol is indexable and has refs from other files in the index,
177 // we disallow rename.
178 return ReasonToReject::UsedOutsideFile;
179 }
180
181 assert(CrossFile);
Haojian Wu7276a442019-06-25 08:43:17 +0000182 if (!Index)
183 return ReasonToReject::NoIndexProvided;
184
Haojian Wu852bafa2019-10-23 14:40:20 +0200185 // Blacklist symbols that are not supported yet in cross-file mode due to the
186 // limitations of our index.
Kirill Bobyrev9091f062019-12-03 10:12:17 +0100187 // FIXME: Renaming templates requires to rename all related specializations,
188 // our index doesn't have this information.
Haojian Wu852bafa2019-10-23 14:40:20 +0200189 if (RenameDecl.getDescribedTemplate())
190 return ReasonToReject::UnsupportedSymbol;
191
Kirill Bobyrev9091f062019-12-03 10:12:17 +0100192 // FIXME: Renaming virtual methods requires to rename all overridens in
193 // subclasses, our index doesn't have this information.
194 // Note: Within-file rename does support this through the AST.
Haojian Wu852bafa2019-10-23 14:40:20 +0200195 if (const auto *S = llvm::dyn_cast<CXXMethodDecl>(&RenameDecl)) {
196 if (S->isVirtual())
197 return ReasonToReject::UnsupportedSymbol;
198 }
199 return None;
Haojian Wu7276a442019-06-25 08:43:17 +0000200}
201
Haojian Wu9d34f452019-07-01 09:26:48 +0000202llvm::Error makeError(ReasonToReject Reason) {
203 auto Message = [](ReasonToReject Reason) {
204 switch (Reason) {
Haojian Wu852bafa2019-10-23 14:40:20 +0200205 case ReasonToReject::NoSymbolFound:
Haojian Wu8b491732019-08-09 10:55:22 +0000206 return "there is no symbol at the given location";
Haojian Wu852bafa2019-10-23 14:40:20 +0200207 case ReasonToReject::NoIndexProvided:
Haojian Wu08cce032019-11-28 11:39:48 +0100208 return "no index provided";
Haojian Wu852bafa2019-10-23 14:40:20 +0200209 case ReasonToReject::UsedOutsideFile:
Haojian Wu9d34f452019-07-01 09:26:48 +0000210 return "the symbol is used outside main file";
Haojian Wu852bafa2019-10-23 14:40:20 +0200211 case ReasonToReject::NonIndexable:
Haojian Wu9d34f452019-07-01 09:26:48 +0000212 return "symbol may be used in other files (not eligible for indexing)";
Haojian Wu852bafa2019-10-23 14:40:20 +0200213 case ReasonToReject::UnsupportedSymbol:
Haojian Wu9d34f452019-07-01 09:26:48 +0000214 return "symbol is not a supported kind (e.g. namespace, macro)";
Haojian Wu7db12302019-11-19 10:10:43 +0100215 case AmbiguousSymbol:
216 return "there are multiple symbols at the given location";
Haojian Wu9d34f452019-07-01 09:26:48 +0000217 }
218 llvm_unreachable("unhandled reason kind");
219 };
220 return llvm::make_error<llvm::StringError>(
221 llvm::formatv("Cannot rename symbol: {0}", Message(Reason)),
222 llvm::inconvertibleErrorCode());
223}
224
Haojian Wu8b491732019-08-09 10:55:22 +0000225// Return all rename occurrences in the main file.
Haojian Wu7db12302019-11-19 10:10:43 +0100226std::vector<SourceLocation> findOccurrencesWithinFile(ParsedAST &AST,
227 const NamedDecl &ND) {
Kirill Bobyrevec618822019-12-12 13:10:59 +0100228 // If the cursor is at the underlying CXXRecordDecl of the
229 // ClassTemplateDecl, ND will be the CXXRecordDecl. In this case, we need to
230 // get the primary template maunally.
Haojian Wu7db12302019-11-19 10:10:43 +0100231 // getUSRsForDeclaration will find other related symbols, e.g. virtual and its
232 // overriddens, primary template and all explicit specializations.
Kirill Bobyrev9091f062019-12-03 10:12:17 +0100233 // FIXME: Get rid of the remaining tooling APIs.
Haojian Wu3de9a5d2020-01-20 14:14:52 +0100234 const auto *RenameDecl =
Kirill Bobyrevec618822019-12-12 13:10:59 +0100235 ND.getDescribedTemplate() ? ND.getDescribedTemplate() : &ND;
236 std::vector<std::string> RenameUSRs =
237 tooling::getUSRsForDeclaration(RenameDecl, AST.getASTContext());
Haojian Wu7db12302019-11-19 10:10:43 +0100238 llvm::DenseSet<SymbolID> TargetIDs;
239 for (auto &USR : RenameUSRs)
240 TargetIDs.insert(SymbolID(USR));
241
242 std::vector<SourceLocation> Results;
Haojian Wu8b491732019-08-09 10:55:22 +0000243 for (Decl *TopLevelDecl : AST.getLocalTopLevelDecls()) {
Haojian Wu7db12302019-11-19 10:10:43 +0100244 findExplicitReferences(TopLevelDecl, [&](ReferenceLoc Ref) {
245 if (Ref.Targets.empty())
246 return;
247 for (const auto *Target : Ref.Targets) {
248 auto ID = getSymbolID(Target);
249 if (!ID || TargetIDs.find(*ID) == TargetIDs.end())
250 return;
251 }
252 Results.push_back(Ref.NameLoc);
253 });
Haojian Wu8b491732019-08-09 10:55:22 +0000254 }
Haojian Wu7db12302019-11-19 10:10:43 +0100255
256 return Results;
Haojian Wu8b491732019-08-09 10:55:22 +0000257}
258
Haojian Wu852bafa2019-10-23 14:40:20 +0200259// AST-based rename, it renames all occurrences in the main file.
Sam McCallc0949122019-05-07 07:11:56 +0000260llvm::Expected<tooling::Replacements>
Haojian Wu852bafa2019-10-23 14:40:20 +0200261renameWithinFile(ParsedAST &AST, const NamedDecl &RenameDecl,
262 llvm::StringRef NewName) {
Sam McCallb2a984c02019-09-04 10:15:27 +0000263 const SourceManager &SM = AST.getSourceManager();
Haojian Wu7276a442019-06-25 08:43:17 +0000264
Haojian Wu8b491732019-08-09 10:55:22 +0000265 tooling::Replacements FilteredChanges;
Haojian Wu852bafa2019-10-23 14:40:20 +0200266 for (SourceLocation Loc : findOccurrencesWithinFile(AST, RenameDecl)) {
Haojian Wu7db12302019-11-19 10:10:43 +0100267 SourceLocation RenameLoc = Loc;
268 // We don't rename in any macro bodies, but we allow rename the symbol
269 // spelled in a top-level macro argument in the main file.
270 if (RenameLoc.isMacroID()) {
271 if (isInMacroBody(SM, RenameLoc))
272 continue;
273 RenameLoc = SM.getSpellingLoc(Loc);
274 }
275 // Filter out locations not from main file.
276 // We traverse only main file decls, but locations could come from an
277 // non-preamble #include file e.g.
278 // void test() {
279 // int f^oo;
280 // #include "use_foo.inc"
281 // }
282 if (!isInsideMainFile(RenameLoc, SM))
Haojian Wu8b491732019-08-09 10:55:22 +0000283 continue;
Haojian Wu8b491732019-08-09 10:55:22 +0000284 if (auto Err = FilteredChanges.add(tooling::Replacement(
Haojian Wu7db12302019-11-19 10:10:43 +0100285 SM, CharSourceRange::getTokenRange(RenameLoc), NewName)))
Haojian Wu8b491732019-08-09 10:55:22 +0000286 return std::move(Err);
Sam McCallc0949122019-05-07 07:11:56 +0000287 }
288 return FilteredChanges;
289}
290
Haojian Wu852bafa2019-10-23 14:40:20 +0200291Range toRange(const SymbolLocation &L) {
292 Range R;
293 R.start.line = L.Start.line();
294 R.start.character = L.Start.column();
295 R.end.line = L.End.line();
296 R.end.character = L.End.column();
297 return R;
Bill Wendling936de1c2019-12-02 14:05:28 -0800298}
Haojian Wu852bafa2019-10-23 14:40:20 +0200299
Kirill Bobyrev9091f062019-12-03 10:12:17 +0100300// Return all rename occurrences (using the index) outside of the main file,
Haojian Wu852bafa2019-10-23 14:40:20 +0200301// grouped by the absolute file path.
Haojian Wu3c3aca22019-11-28 12:47:32 +0100302llvm::Expected<llvm::StringMap<std::vector<Range>>>
Haojian Wu852bafa2019-10-23 14:40:20 +0200303findOccurrencesOutsideFile(const NamedDecl &RenameDecl,
304 llvm::StringRef MainFile, const SymbolIndex &Index) {
305 RefsRequest RQuest;
306 RQuest.IDs.insert(*getSymbolID(&RenameDecl));
307
Haojian Wu3c3aca22019-11-28 12:47:32 +0100308 // Absolute file path => rename occurrences in that file.
Haojian Wu852bafa2019-10-23 14:40:20 +0200309 llvm::StringMap<std::vector<Range>> AffectedFiles;
Kirill Bobyrev9091f062019-12-03 10:12:17 +0100310 // FIXME: Make the limit customizable.
Haojian Wu3c3aca22019-11-28 12:47:32 +0100311 static constexpr size_t MaxLimitFiles = 50;
312 bool HasMore = Index.refs(RQuest, [&](const Ref &R) {
313 if (AffectedFiles.size() > MaxLimitFiles)
314 return;
Kirill Bobyrev10540e42020-02-06 10:28:47 +0100315 if ((R.Kind & RefKind::Spelled) == RefKind::Unknown)
316 return;
Haojian Wu852bafa2019-10-23 14:40:20 +0200317 if (auto RefFilePath = filePath(R.Location, /*HintFilePath=*/MainFile)) {
318 if (*RefFilePath != MainFile)
319 AffectedFiles[*RefFilePath].push_back(toRange(R.Location));
320 }
321 });
Haojian Wu3c3aca22019-11-28 12:47:32 +0100322
323 if (AffectedFiles.size() > MaxLimitFiles)
324 return llvm::make_error<llvm::StringError>(
325 llvm::formatv("The number of affected files exceeds the max limit {0}",
326 MaxLimitFiles),
327 llvm::inconvertibleErrorCode());
328 if (HasMore) {
329 return llvm::make_error<llvm::StringError>(
330 llvm::formatv("The symbol {0} has too many occurrences",
331 RenameDecl.getQualifiedNameAsString()),
332 llvm::inconvertibleErrorCode());
333 }
Haojian Wuf0004aa2019-12-10 22:15:29 +0100334 // Sort and deduplicate the results, in case that index returns duplications.
335 for (auto &FileAndOccurrences : AffectedFiles) {
336 auto &Ranges = FileAndOccurrences.getValue();
337 llvm::sort(Ranges);
338 Ranges.erase(std::unique(Ranges.begin(), Ranges.end()), Ranges.end());
339 }
Haojian Wu852bafa2019-10-23 14:40:20 +0200340 return AffectedFiles;
341}
342
Haojian Wu852bafa2019-10-23 14:40:20 +0200343// Index-based rename, it renames all occurrences outside of the main file.
344//
345// The cross-file rename is purely based on the index, as we don't want to
346// build all ASTs for affected files, which may cause a performance hit.
347// We choose to trade off some correctness for performance and scalability.
348//
349// Clangd builds a dynamic index for all opened files on top of the static
350// index of the whole codebase. Dynamic index is up-to-date (respects dirty
351// buffers) as long as clangd finishes processing opened files, while static
352// index (background index) is relatively stale. We choose the dirty buffers
353// as the file content we rename on, and fallback to file content on disk if
354// there is no dirty buffer.
355//
Kirill Bobyrev9091f062019-12-03 10:12:17 +0100356// FIXME: Our index may return implicit references, which are not eligible for
357// rename, we should filter out these references.
Haojian Wu852bafa2019-10-23 14:40:20 +0200358llvm::Expected<FileEdits> renameOutsideFile(
359 const NamedDecl &RenameDecl, llvm::StringRef MainFilePath,
360 llvm::StringRef NewName, const SymbolIndex &Index,
361 llvm::function_ref<llvm::Expected<std::string>(PathRef)> GetFileContent) {
362 auto AffectedFiles =
363 findOccurrencesOutsideFile(RenameDecl, MainFilePath, Index);
Haojian Wu3c3aca22019-11-28 12:47:32 +0100364 if (!AffectedFiles)
365 return AffectedFiles.takeError();
Haojian Wu852bafa2019-10-23 14:40:20 +0200366 FileEdits Results;
Haojian Wu3c3aca22019-11-28 12:47:32 +0100367 for (auto &FileAndOccurrences : *AffectedFiles) {
Haojian Wu852bafa2019-10-23 14:40:20 +0200368 llvm::StringRef FilePath = FileAndOccurrences.first();
369
370 auto AffectedFileCode = GetFileContent(FilePath);
371 if (!AffectedFileCode) {
372 elog("Fail to read file content: {0}", AffectedFileCode.takeError());
373 continue;
374 }
Haojian Wu891f8222019-12-09 17:00:51 +0100375 auto RenameRanges =
376 adjustRenameRanges(*AffectedFileCode, RenameDecl.getNameAsString(),
377 std::move(FileAndOccurrences.second),
378 RenameDecl.getASTContext().getLangOpts());
379 if (!RenameRanges) {
Kirill Bobyrev3b9715c2019-12-16 10:33:56 +0100380 // Our heuristics fails to adjust rename ranges to the current state of
Haojian Wu891f8222019-12-09 17:00:51 +0100381 // the file, it is most likely the index is stale, so we give up the
382 // entire rename.
383 return llvm::make_error<llvm::StringError>(
384 llvm::formatv("Index results don't match the content of file {0} "
385 "(the index may be stale)",
386 FilePath),
387 llvm::inconvertibleErrorCode());
388 }
Haojian Wu66ab9322019-11-28 16:48:49 +0100389 auto RenameEdit =
Haojian Wu891f8222019-12-09 17:00:51 +0100390 buildRenameEdit(FilePath, *AffectedFileCode, *RenameRanges, NewName);
Haojian Wu88053162019-11-19 15:23:36 +0100391 if (!RenameEdit) {
392 return llvm::make_error<llvm::StringError>(
393 llvm::formatv("fail to build rename edit for file {0}: {1}", FilePath,
394 llvm::toString(RenameEdit.takeError())),
395 llvm::inconvertibleErrorCode());
396 }
Haojian Wu852bafa2019-10-23 14:40:20 +0200397 if (!RenameEdit->Replacements.empty())
398 Results.insert({FilePath, std::move(*RenameEdit)});
399 }
400 return Results;
401}
402
Kirill Bobyrev3b9715c2019-12-16 10:33:56 +0100403// A simple edit is either changing line or column, but not both.
Haojian Wu891f8222019-12-09 17:00:51 +0100404bool impliesSimpleEdit(const Position &LHS, const Position &RHS) {
405 return LHS.line == RHS.line || LHS.character == RHS.character;
406}
407
408// Performs a DFS to enumerate all possible near-miss matches.
409// It finds the locations where the indexed occurrences are now spelled in
410// Lexed occurrences, a near miss is defined as:
411// - a near miss maps all of the **name** occurrences from the index onto a
412// *subset* of lexed occurrences (we allow a single name refers to more
413// than one symbol)
414// - all indexed occurrences must be mapped, and Result must be distinct and
415// preseve order (only support detecting simple edits to ensure a
416// robust mapping)
417// - each indexed -> lexed occurrences mapping correspondence may change the
418// *line* or *column*, but not both (increases chance of a robust mapping)
419void findNearMiss(
420 std::vector<size_t> &PartialMatch, ArrayRef<Range> IndexedRest,
421 ArrayRef<Range> LexedRest, int LexedIndex, int &Fuel,
422 llvm::function_ref<void(const std::vector<size_t> &)> MatchedCB) {
423 if (--Fuel < 0)
424 return;
425 if (IndexedRest.size() > LexedRest.size())
426 return;
427 if (IndexedRest.empty()) {
428 MatchedCB(PartialMatch);
429 return;
430 }
431 if (impliesSimpleEdit(IndexedRest.front().start, LexedRest.front().start)) {
432 PartialMatch.push_back(LexedIndex);
433 findNearMiss(PartialMatch, IndexedRest.drop_front(), LexedRest.drop_front(),
434 LexedIndex + 1, Fuel, MatchedCB);
435 PartialMatch.pop_back();
436 }
437 findNearMiss(PartialMatch, IndexedRest, LexedRest.drop_front(),
438 LexedIndex + 1, Fuel, MatchedCB);
439}
440
Haojian Wu852bafa2019-10-23 14:40:20 +0200441} // namespace
442
443llvm::Expected<FileEdits> rename(const RenameInputs &RInputs) {
444 ParsedAST &AST = RInputs.AST;
445 const SourceManager &SM = AST.getSourceManager();
446 llvm::StringRef MainFileCode = SM.getBufferData(SM.getMainFileID());
447 auto GetFileContent = [&RInputs,
448 &SM](PathRef AbsPath) -> llvm::Expected<std::string> {
449 llvm::Optional<std::string> DirtyBuffer;
450 if (RInputs.GetDirtyBuffer &&
451 (DirtyBuffer = RInputs.GetDirtyBuffer(AbsPath)))
452 return std::move(*DirtyBuffer);
453
454 auto Content =
455 SM.getFileManager().getVirtualFileSystem().getBufferForFile(AbsPath);
456 if (!Content)
457 return llvm::createStringError(
458 llvm::inconvertibleErrorCode(),
459 llvm::formatv("Fail to open file {0}: {1}", AbsPath,
460 Content.getError().message()));
461 if (!*Content)
462 return llvm::createStringError(
463 llvm::inconvertibleErrorCode(),
464 llvm::formatv("Got no buffer for file {0}", AbsPath));
465
466 return (*Content)->getBuffer().str();
467 };
Kirill Bobyrevec618822019-12-12 13:10:59 +0100468 // Try to find the tokens adjacent to the cursor position.
469 auto Loc = sourceLocationInMainFile(SM, RInputs.Pos);
470 if (!Loc)
471 return Loc.takeError();
472 const syntax::Token *IdentifierToken =
473 spelledIdentifierTouching(*Loc, AST.getTokens());
474 // Renames should only triggered on identifiers.
475 if (!IdentifierToken)
476 return makeError(ReasonToReject::NoSymbolFound);
Kirill Bobyrev9091f062019-12-03 10:12:17 +0100477 // FIXME: Renaming macros is not supported yet, the macro-handling code should
Haojian Wu852bafa2019-10-23 14:40:20 +0200478 // be moved to rename tooling library.
Kirill Bobyrevec618822019-12-12 13:10:59 +0100479 if (locateMacroAt(IdentifierToken->location(), AST.getPreprocessor()))
Haojian Wu852bafa2019-10-23 14:40:20 +0200480 return makeError(ReasonToReject::UnsupportedSymbol);
481
Kirill Bobyrevec618822019-12-12 13:10:59 +0100482 auto DeclsUnderCursor = locateDeclAt(AST, IdentifierToken->location());
Haojian Wu852bafa2019-10-23 14:40:20 +0200483 if (DeclsUnderCursor.empty())
484 return makeError(ReasonToReject::NoSymbolFound);
485 if (DeclsUnderCursor.size() > 1)
486 return makeError(ReasonToReject::AmbiguousSymbol);
487
Haojian Wu0d893fd2020-01-27 10:39:55 +0100488 const auto &RenameDecl =
489 llvm::cast<NamedDecl>(*(*DeclsUnderCursor.begin())->getCanonicalDecl());
490 auto Reject = renameable(RenameDecl, RInputs.MainFilePath, RInputs.Index,
491 RInputs.AllowCrossFile);
Haojian Wu852bafa2019-10-23 14:40:20 +0200492 if (Reject)
493 return makeError(*Reject);
494
Kirill Bobyrev9091f062019-12-03 10:12:17 +0100495 // We have two implementations of the rename:
Haojian Wu852bafa2019-10-23 14:40:20 +0200496 // - AST-based rename: used for renaming local symbols, e.g. variables
497 // defined in a function body;
498 // - index-based rename: used for renaming non-local symbols, and not
499 // feasible for local symbols (as by design our index don't index these
500 // symbols by design;
501 // To make cross-file rename work for local symbol, we use a hybrid solution:
502 // - run AST-based rename on the main file;
503 // - run index-based rename on other affected files;
Haojian Wu0d893fd2020-01-27 10:39:55 +0100504 auto MainFileRenameEdit = renameWithinFile(AST, RenameDecl, RInputs.NewName);
Haojian Wu852bafa2019-10-23 14:40:20 +0200505 if (!MainFileRenameEdit)
506 return MainFileRenameEdit.takeError();
507
508 if (!RInputs.AllowCrossFile) {
Kirill Bobyrev9091f062019-12-03 10:12:17 +0100509 // Within-file rename: just return the main file results.
Haojian Wu852bafa2019-10-23 14:40:20 +0200510 return FileEdits(
511 {std::make_pair(RInputs.MainFilePath,
512 Edit{MainFileCode, std::move(*MainFileRenameEdit)})});
513 }
514
515 FileEdits Results;
Kirill Bobyrev9091f062019-12-03 10:12:17 +0100516 // Renameable safely guards us that at this point we are renaming a local
517 // symbol if we don't have index.
Haojian Wu852bafa2019-10-23 14:40:20 +0200518 if (RInputs.Index) {
519 auto OtherFilesEdits =
Haojian Wu0d893fd2020-01-27 10:39:55 +0100520 renameOutsideFile(RenameDecl, RInputs.MainFilePath, RInputs.NewName,
Haojian Wu852bafa2019-10-23 14:40:20 +0200521 *RInputs.Index, GetFileContent);
522 if (!OtherFilesEdits)
523 return OtherFilesEdits.takeError();
524 Results = std::move(*OtherFilesEdits);
525 }
526 // Attach the rename edits for the main file.
527 Results.try_emplace(RInputs.MainFilePath, MainFileCode,
528 std::move(*MainFileRenameEdit));
529 return Results;
530}
531
Haojian Wu66ab9322019-11-28 16:48:49 +0100532llvm::Expected<Edit> buildRenameEdit(llvm::StringRef AbsFilePath,
533 llvm::StringRef InitialCode,
Haojian Wu88053162019-11-19 15:23:36 +0100534 std::vector<Range> Occurrences,
535 llvm::StringRef NewName) {
Haojian Wuf0004aa2019-12-10 22:15:29 +0100536 assert(std::is_sorted(Occurrences.begin(), Occurrences.end()));
537 assert(std::unique(Occurrences.begin(), Occurrences.end()) ==
538 Occurrences.end() &&
539 "Occurrences must be unique");
540
Haojian Wu88053162019-11-19 15:23:36 +0100541 // These two always correspond to the same position.
542 Position LastPos{0, 0};
543 size_t LastOffset = 0;
544
545 auto Offset = [&](const Position &P) -> llvm::Expected<size_t> {
546 assert(LastPos <= P && "malformed input");
547 Position Shifted = {
548 P.line - LastPos.line,
549 P.line > LastPos.line ? P.character : P.character - LastPos.character};
550 auto ShiftedOffset =
551 positionToOffset(InitialCode.substr(LastOffset), Shifted);
552 if (!ShiftedOffset)
553 return llvm::make_error<llvm::StringError>(
554 llvm::formatv("fail to convert the position {0} to offset ({1})", P,
555 llvm::toString(ShiftedOffset.takeError())),
556 llvm::inconvertibleErrorCode());
557 LastPos = P;
558 LastOffset += *ShiftedOffset;
559 return LastOffset;
560 };
561
562 std::vector<std::pair</*start*/ size_t, /*end*/ size_t>> OccurrencesOffsets;
563 for (const auto &R : Occurrences) {
564 auto StartOffset = Offset(R.start);
565 if (!StartOffset)
566 return StartOffset.takeError();
567 auto EndOffset = Offset(R.end);
568 if (!EndOffset)
569 return EndOffset.takeError();
570 OccurrencesOffsets.push_back({*StartOffset, *EndOffset});
571 }
572
573 tooling::Replacements RenameEdit;
574 for (const auto &R : OccurrencesOffsets) {
575 auto ByteLength = R.second - R.first;
576 if (auto Err = RenameEdit.add(
Haojian Wu66ab9322019-11-28 16:48:49 +0100577 tooling::Replacement(AbsFilePath, R.first, ByteLength, NewName)))
Haojian Wu88053162019-11-19 15:23:36 +0100578 return std::move(Err);
579 }
580 return Edit(InitialCode, std::move(RenameEdit));
581}
582
Haojian Wu891f8222019-12-09 17:00:51 +0100583// Details:
584// - lex the draft code to get all rename candidates, this yields a superset
585// of candidates.
586// - apply range patching heuristics to generate "authoritative" occurrences,
587// cases we consider:
588// (a) index returns a subset of candidates, we use the indexed results.
589// - fully equal, we are sure the index is up-to-date
590// - proper subset, index is correct in most cases? there may be false
591// positives (e.g. candidates got appended), but rename is still safe
592// (b) index returns non-candidate results, we attempt to map the indexed
593// ranges onto candidates in a plausible way (e.g. guess that lines
594// were inserted). If such a "near miss" is found, the rename is still
595// possible
596llvm::Optional<std::vector<Range>>
597adjustRenameRanges(llvm::StringRef DraftCode, llvm::StringRef Identifier,
598 std::vector<Range> Indexed, const LangOptions &LangOpts) {
599 assert(!Indexed.empty());
Haojian Wuf0004aa2019-12-10 22:15:29 +0100600 assert(std::is_sorted(Indexed.begin(), Indexed.end()));
Haojian Wu891f8222019-12-09 17:00:51 +0100601 std::vector<Range> Lexed =
602 collectIdentifierRanges(Identifier, DraftCode, LangOpts);
Haojian Wu891f8222019-12-09 17:00:51 +0100603 llvm::sort(Lexed);
604 return getMappedRanges(Indexed, Lexed);
605}
606
607llvm::Optional<std::vector<Range>> getMappedRanges(ArrayRef<Range> Indexed,
608 ArrayRef<Range> Lexed) {
609 assert(!Indexed.empty());
610 assert(std::is_sorted(Indexed.begin(), Indexed.end()));
611 assert(std::is_sorted(Lexed.begin(), Lexed.end()));
612
613 if (Indexed.size() > Lexed.size()) {
614 vlog("The number of lexed occurrences is less than indexed occurrences");
615 return llvm::None;
616 }
617 // Fast check for the special subset case.
618 if (std::includes(Indexed.begin(), Indexed.end(), Lexed.begin(), Lexed.end()))
619 return Indexed.vec();
620
621 std::vector<size_t> Best;
622 size_t BestCost = std::numeric_limits<size_t>::max();
623 bool HasMultiple = 0;
624 std::vector<size_t> ResultStorage;
625 int Fuel = 10000;
626 findNearMiss(ResultStorage, Indexed, Lexed, 0, Fuel,
627 [&](const std::vector<size_t> &Matched) {
628 size_t MCost =
629 renameRangeAdjustmentCost(Indexed, Lexed, Matched);
630 if (MCost < BestCost) {
631 BestCost = MCost;
632 Best = std::move(Matched);
633 HasMultiple = false; // reset
634 return;
635 }
636 if (MCost == BestCost)
637 HasMultiple = true;
638 });
639 if (HasMultiple) {
640 vlog("The best near miss is not unique.");
641 return llvm::None;
642 }
643 if (Best.empty()) {
644 vlog("Didn't find a near miss.");
645 return llvm::None;
646 }
647 std::vector<Range> Mapped;
648 for (auto I : Best)
649 Mapped.push_back(Lexed[I]);
650 return Mapped;
651}
652
653// The cost is the sum of the implied edit sizes between successive diffs, only
654// simple edits are considered:
655// - insert/remove a line (change line offset)
656// - insert/remove a character on an existing line (change column offset)
657//
658// Example I, total result is 1 + 1 = 2.
659// diff[0]: line + 1 <- insert a line before edit 0.
660// diff[1]: line + 1
661// diff[2]: line + 1
662// diff[3]: line + 2 <- insert a line before edits 2 and 3.
663//
664// Example II, total result is 1 + 1 + 1 = 3.
665// diff[0]: line + 1 <- insert a line before edit 0.
666// diff[1]: column + 1 <- remove a line between edits 0 and 1, and insert a
667// character on edit 1.
668size_t renameRangeAdjustmentCost(ArrayRef<Range> Indexed, ArrayRef<Range> Lexed,
669 ArrayRef<size_t> MappedIndex) {
670 assert(Indexed.size() == MappedIndex.size());
671 assert(std::is_sorted(Indexed.begin(), Indexed.end()));
672 assert(std::is_sorted(Lexed.begin(), Lexed.end()));
673
674 int LastLine = -1;
675 int LastDLine = 0, LastDColumn = 0;
676 int Cost = 0;
677 for (size_t I = 0; I < Indexed.size(); ++I) {
678 int DLine = Indexed[I].start.line - Lexed[MappedIndex[I]].start.line;
679 int DColumn =
680 Indexed[I].start.character - Lexed[MappedIndex[I]].start.character;
681 int Line = Indexed[I].start.line;
682 if (Line != LastLine)
Kazuaki Ishizakib7ecf1c2020-01-04 10:28:41 -0500683 LastDColumn = 0; // column offsets don't carry cross lines.
Haojian Wu891f8222019-12-09 17:00:51 +0100684 Cost += abs(DLine - LastDLine) + abs(DColumn - LastDColumn);
685 std::tie(LastLine, LastDLine, LastDColumn) = std::tie(Line, DLine, DColumn);
686 }
687 return Cost;
688}
689
Sam McCallc0949122019-05-07 07:11:56 +0000690} // namespace clangd
691} // namespace clang