1//===--- DefineOutline.cpp ---------------------------------------*- 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
9#include "AST.h"
10#include "FindTarget.h"
11#include "HeaderSourceSwitch.h"
12#include "ParsedAST.h"
13#include "Selection.h"
14#include "SourceCode.h"
15#include "refactor/Tweak.h"
16#include "support/Logger.h"
17#include "support/Path.h"
18#include "clang/AST/ASTTypeTraits.h"
19#include "clang/AST/Attr.h"
20#include "clang/AST/Decl.h"
21#include "clang/AST/DeclBase.h"
22#include "clang/AST/DeclCXX.h"
23#include "clang/AST/DeclTemplate.h"
24#include "clang/AST/Stmt.h"
25#include "clang/Basic/SourceLocation.h"
26#include "clang/Basic/SourceManager.h"
27#include "clang/Basic/TokenKinds.h"
28#include "clang/Tooling/Core/Replacement.h"
29#include "clang/Tooling/Syntax/Tokens.h"
30#include "llvm/ADT/STLExtras.h"
31#include "llvm/ADT/StringRef.h"
32#include "llvm/Support/Casting.h"
33#include "llvm/Support/Error.h"
34#include <cstddef>
35#include <optional>
36#include <string>
37
38namespace clang {
39namespace clangd {
40namespace {
41
42// Deduces the FunctionDecl from a selection. Requires either the function body
43// or the function decl to be selected. Returns null if none of the above
44// criteria is met.
45// FIXME: This is shared with define inline, move them to a common header once
46// we have a place for such.
47const FunctionDecl *getSelectedFunction(const SelectionTree::Node *SelNode) {
48 if (!SelNode)
49 return nullptr;
50 const DynTypedNode &AstNode = SelNode->ASTNode;
51 if (const FunctionDecl *FD = AstNode.get<FunctionDecl>())
52 return FD;
53 if (AstNode.get<CompoundStmt>() &&
54 SelNode->Selected == SelectionTree::Complete) {
55 if (const SelectionTree::Node *P = SelNode->Parent)
56 return P->ASTNode.get<FunctionDecl>();
57 }
58 return nullptr;
59}
60
61std::optional<Path> getSourceFile(llvm::StringRef FileName,
62 const Tweak::Selection &Sel) {
63 assert(Sel.FS);
64 if (auto Source = getCorrespondingHeaderOrSource(OriginalFile: FileName, VFS: Sel.FS))
65 return *Source;
66 return getCorrespondingHeaderOrSource(OriginalFile: FileName, AST&: *Sel.AST, Index: Sel.Index);
67}
68
69// Synthesize a DeclContext for TargetNS from CurContext. TargetNS must be empty
70// for global namespace, and endwith "::" otherwise.
71// Returns std::nullopt if TargetNS is not a prefix of CurContext.
72std::optional<const DeclContext *>
73findContextForNS(llvm::StringRef TargetNS, const DeclContext *CurContext) {
74 assert(TargetNS.empty() || TargetNS.ends_with("::"));
75 // Skip any non-namespace contexts, e.g. TagDecls, functions/methods.
76 CurContext = CurContext->getEnclosingNamespaceContext();
77 // If TargetNS is empty, it means global ns, which is translation unit.
78 if (TargetNS.empty()) {
79 while (!CurContext->isTranslationUnit())
80 CurContext = CurContext->getParent();
81 return CurContext;
82 }
83 // Otherwise we need to drop any trailing namespaces from CurContext until
84 // we reach TargetNS.
85 std::string TargetContextNS =
86 CurContext->isNamespace()
87 ? llvm::cast<NamespaceDecl>(Val: CurContext)->getQualifiedNameAsString()
88 : "";
89 TargetContextNS.append(s: "::");
90
91 llvm::StringRef CurrentContextNS(TargetContextNS);
92 // If TargetNS is not a prefix of CurrentContext, there's no way to reach
93 // it.
94 if (!CurrentContextNS.starts_with(Prefix: TargetNS))
95 return std::nullopt;
96
97 while (CurrentContextNS != TargetNS) {
98 CurContext = CurContext->getParent();
99 // These colons always exists since TargetNS is a prefix of
100 // CurrentContextNS, it ends with "::" and they are not equal.
101 CurrentContextNS = CurrentContextNS.take_front(
102 N: CurrentContextNS.drop_back(N: 2).rfind(Str: "::") + 2);
103 }
104 return CurContext;
105}
106
107// Returns source code for FD after applying Replacements.
108// FIXME: Make the function take a parameter to return only the function body,
109// afterwards it can be shared with define-inline code action.
110llvm::Expected<std::string>
111getFunctionSourceAfterReplacements(const FunctionDecl *FD,
112 const tooling::Replacements &Replacements) {
113 const auto &SM = FD->getASTContext().getSourceManager();
114 auto OrigFuncRange = toHalfOpenFileRange(
115 SM, FD->getASTContext().getLangOpts(), FD->getSourceRange());
116 if (!OrigFuncRange)
117 return error(Fmt: "Couldn't get range for function.");
118 assert(!FD->getDescribedFunctionTemplate() &&
119 "Define out-of-line doesn't apply to function templates.");
120
121 // Get new begin and end positions for the qualified function definition.
122 unsigned FuncBegin = SM.getFileOffset(OrigFuncRange->getBegin());
123 unsigned FuncEnd = Replacements.getShiftedCodePosition(
124 Position: SM.getFileOffset(OrigFuncRange->getEnd()));
125
126 // Trim the result to function definition.
127 auto QualifiedFunc = tooling::applyAllReplacements(
128 SM.getBufferData(SM.getMainFileID()), Replacements);
129 if (!QualifiedFunc)
130 return QualifiedFunc.takeError();
131 return QualifiedFunc->substr(FuncBegin, FuncEnd - FuncBegin + 1);
132}
133
134// Returns replacements to delete tokens with kind `Kind` in the range
135// `FromRange`. Removes matching instances of given token preceeding the
136// function defition.
137llvm::Expected<tooling::Replacements>
138deleteTokensWithKind(const syntax::TokenBuffer &TokBuf, tok::TokenKind Kind,
139 SourceRange FromRange) {
140 tooling::Replacements DelKeywordCleanups;
141 llvm::Error Errors = llvm::Error::success();
142 bool FoundAny = false;
143 for (const auto &Tok : TokBuf.expandedTokens(R: FromRange)) {
144 if (Tok.kind() != Kind)
145 continue;
146 FoundAny = true;
147 auto Spelling = TokBuf.spelledForExpanded(Expanded: llvm::ArrayRef(Tok));
148 if (!Spelling) {
149 Errors = llvm::joinErrors(
150 E1: std::move(Errors),
151 E2: error(Fmt: "define outline: couldn't remove `{0}` keyword.",
152 Vals: tok::getKeywordSpelling(Kind)));
153 break;
154 }
155 auto &SM = TokBuf.sourceManager();
156 CharSourceRange DelRange =
157 syntax::Token::range(SM, First: Spelling->front(), Last: Spelling->back())
158 .toCharRange(SM);
159 if (auto Err =
160 DelKeywordCleanups.add(R: tooling::Replacement(SM, DelRange, "")))
161 Errors = llvm::joinErrors(E1: std::move(Errors), E2: std::move(Err));
162 }
163 if (!FoundAny) {
164 Errors = llvm::joinErrors(
165 E1: std::move(Errors),
166 E2: error(Fmt: "define outline: couldn't find `{0}` keyword to remove.",
167 Vals: tok::getKeywordSpelling(Kind)));
168 }
169
170 if (Errors)
171 return std::move(Errors);
172 return DelKeywordCleanups;
173}
174
175// Creates a modified version of function definition that can be inserted at a
176// different location, qualifies return value and function name to achieve that.
177// Contains function signature, except defaulted parameter arguments, body and
178// template parameters if applicable. No need to qualify parameters, as they are
179// looked up in the context containing the function/method.
180// FIXME: Drop attributes in function signature.
181llvm::Expected<std::string>
182getFunctionSourceCode(const FunctionDecl *FD, llvm::StringRef TargetNamespace,
183 const syntax::TokenBuffer &TokBuf,
184 const HeuristicResolver *Resolver) {
185 auto &AST = FD->getASTContext();
186 auto &SM = AST.getSourceManager();
187 auto TargetContext = findContextForNS(TargetNamespace, FD->getDeclContext());
188 if (!TargetContext)
189 return error(Fmt: "define outline: couldn't find a context for target");
190
191 llvm::Error Errors = llvm::Error::success();
192 tooling::Replacements DeclarationCleanups;
193
194 // Finds the first unqualified name in function return type and name, then
195 // qualifies those to be valid in TargetContext.
196 findExplicitReferences(
197 FD,
198 [&](ReferenceLoc Ref) {
199 // It is enough to qualify the first qualifier, so skip references with
200 // a qualifier. Also we can't do much if there are no targets or name is
201 // inside a macro body.
202 if (Ref.Qualifier || Ref.Targets.empty() || Ref.NameLoc.isMacroID())
203 return;
204 // Only qualify return type and function name.
205 if (Ref.NameLoc != FD->getReturnTypeSourceRange().getBegin() &&
206 Ref.NameLoc != FD->getLocation())
207 return;
208
209 for (const NamedDecl *ND : Ref.Targets) {
210 if (ND->getDeclContext() != Ref.Targets.front()->getDeclContext()) {
211 elog(Fmt: "Targets from multiple contexts: {0}, {1}",
212 Vals: printQualifiedName(ND: *Ref.Targets.front()),
213 Vals: printQualifiedName(ND: *ND));
214 return;
215 }
216 }
217 const NamedDecl *ND = Ref.Targets.front();
218 const std::string Qualifier =
219 getQualification(AST, *TargetContext,
220 SM.getLocForStartOfFile(SM.getMainFileID()), ND);
221 if (auto Err = DeclarationCleanups.add(
222 tooling::Replacement(SM, Ref.NameLoc, 0, Qualifier)))
223 Errors = llvm::joinErrors(E1: std::move(Errors), E2: std::move(Err));
224 },
225 Resolver);
226
227 // findExplicitReferences doesn't provide references to
228 // constructor/destructors, it only provides references to type names inside
229 // them.
230 // this works for constructors, but doesn't work for destructor as type name
231 // doesn't cover leading `~`, so handle it specially.
232 if (const auto *Destructor = llvm::dyn_cast<CXXDestructorDecl>(Val: FD)) {
233 if (auto Err = DeclarationCleanups.add(tooling::Replacement(
234 SM, Destructor->getLocation(), 0,
235 getQualification(AST, *TargetContext,
236 SM.getLocForStartOfFile(SM.getMainFileID()),
237 Destructor))))
238 Errors = llvm::joinErrors(E1: std::move(Errors), E2: std::move(Err));
239 }
240
241 // Get rid of default arguments, since they should not be specified in
242 // out-of-line definition.
243 for (const auto *PVD : FD->parameters()) {
244 if (!PVD->hasDefaultArg())
245 continue;
246 // Deletion range spans the initializer, usually excluding the `=`.
247 auto DelRange = CharSourceRange::getTokenRange(R: PVD->getDefaultArgRange());
248 // Get all tokens before the default argument.
249 auto Tokens = TokBuf.expandedTokens(R: PVD->getSourceRange())
250 .take_while(Pred: [&SM, &DelRange](const syntax::Token &Tok) {
251 return SM.isBeforeInTranslationUnit(
252 Tok.location(), DelRange.getBegin());
253 });
254 if (TokBuf.expandedTokens(R: DelRange.getAsRange()).front().kind() !=
255 tok::equal) {
256 // Find the last `=` if it isn't included in the initializer, and update
257 // the DelRange to include it.
258 auto Tok =
259 llvm::find_if(Range: llvm::reverse(C&: Tokens), P: [](const syntax::Token &Tok) {
260 return Tok.kind() == tok::equal;
261 });
262 assert(Tok != Tokens.rend());
263 DelRange.setBegin(Tok->location());
264 }
265 if (auto Err =
266 DeclarationCleanups.add(tooling::Replacement(SM, DelRange, "")))
267 Errors = llvm::joinErrors(E1: std::move(Errors), E2: std::move(Err));
268 }
269
270 auto DelAttr = [&](const Attr *A) {
271 if (!A)
272 return;
273 auto AttrTokens =
274 TokBuf.spelledForExpanded(Expanded: TokBuf.expandedTokens(R: A->getRange()));
275 assert(A->getLocation().isValid());
276 if (!AttrTokens || AttrTokens->empty()) {
277 Errors = llvm::joinErrors(
278 E1: std::move(Errors), E2: error(Fmt: "define outline: Can't move out of line as "
279 "function has a macro `{0}` specifier.",
280 Vals: A->getSpelling()));
281 return;
282 }
283 CharSourceRange DelRange =
284 syntax::Token::range(SM, AttrTokens->front(), AttrTokens->back())
285 .toCharRange(SM);
286 if (auto Err =
287 DeclarationCleanups.add(tooling::Replacement(SM, DelRange, "")))
288 Errors = llvm::joinErrors(E1: std::move(Errors), E2: std::move(Err));
289 };
290
291 DelAttr(FD->getAttr<OverrideAttr>());
292 DelAttr(FD->getAttr<FinalAttr>());
293
294 auto DelKeyword = [&](tok::TokenKind Kind, SourceRange FromRange) {
295 auto DelKeywords = deleteTokensWithKind(TokBuf, Kind, FromRange);
296 if (!DelKeywords) {
297 Errors = llvm::joinErrors(E1: std::move(Errors), E2: DelKeywords.takeError());
298 return;
299 }
300 DeclarationCleanups = DeclarationCleanups.merge(Replaces: *DelKeywords);
301 };
302
303 if (FD->isInlineSpecified())
304 DelKeyword(tok::kw_inline, {FD->getBeginLoc(), FD->getLocation()});
305 if (const auto *MD = dyn_cast<CXXMethodDecl>(Val: FD)) {
306 if (MD->isVirtualAsWritten())
307 DelKeyword(tok::kw_virtual, {FD->getBeginLoc(), FD->getLocation()});
308 if (MD->isStatic())
309 DelKeyword(tok::kw_static, {FD->getBeginLoc(), FD->getLocation()});
310 }
311 if (const auto *CD = dyn_cast<CXXConstructorDecl>(Val: FD)) {
312 if (CD->isExplicit())
313 DelKeyword(tok::kw_explicit, {FD->getBeginLoc(), FD->getLocation()});
314 }
315
316 if (Errors)
317 return std::move(Errors);
318 return getFunctionSourceAfterReplacements(FD, Replacements: DeclarationCleanups);
319}
320
321struct InsertionPoint {
322 std::string EnclosingNamespace;
323 size_t Offset;
324};
325// Returns the most natural insertion point for \p QualifiedName in \p Contents.
326// This currently cares about only the namespace proximity, but in feature it
327// should also try to follow ordering of declarations. For example, if decls
328// come in order `foo, bar, baz` then this function should return some point
329// between foo and baz for inserting bar.
330llvm::Expected<InsertionPoint> getInsertionPoint(llvm::StringRef Contents,
331 llvm::StringRef QualifiedName,
332 const LangOptions &LangOpts) {
333 auto Region = getEligiblePoints(Code: Contents, FullyQualifiedName: QualifiedName, LangOpts);
334
335 assert(!Region.EligiblePoints.empty());
336 // FIXME: This selection can be made smarter by looking at the definition
337 // locations for adjacent decls to Source. Unfortunately pseudo parsing in
338 // getEligibleRegions only knows about namespace begin/end events so we
339 // can't match function start/end positions yet.
340 auto Offset = positionToOffset(Code: Contents, P: Region.EligiblePoints.back());
341 if (!Offset)
342 return Offset.takeError();
343 return InsertionPoint{.EnclosingNamespace: Region.EnclosingNamespace, .Offset: *Offset};
344}
345
346// Returns the range that should be deleted from declaration, which always
347// contains function body. In addition to that it might contain constructor
348// initializers.
349SourceRange getDeletionRange(const FunctionDecl *FD,
350 const syntax::TokenBuffer &TokBuf) {
351 auto DeletionRange = FD->getBody()->getSourceRange();
352 if (auto *CD = llvm::dyn_cast<CXXConstructorDecl>(Val: FD)) {
353 // AST doesn't contain the location for ":" in ctor initializers. Therefore
354 // we find it by finding the first ":" before the first ctor initializer.
355 SourceLocation InitStart;
356 // Find the first initializer.
357 for (const auto *CInit : CD->inits()) {
358 // SourceOrder is -1 for implicit initializers.
359 if (CInit->getSourceOrder() != 0)
360 continue;
361 InitStart = CInit->getSourceLocation();
362 break;
363 }
364 if (InitStart.isValid()) {
365 auto Toks = TokBuf.expandedTokens(CD->getSourceRange());
366 // Drop any tokens after the initializer.
367 Toks = Toks.take_while([&TokBuf, &InitStart](const syntax::Token &Tok) {
368 return TokBuf.sourceManager().isBeforeInTranslationUnit(LHS: Tok.location(),
369 RHS: InitStart);
370 });
371 // Look for the first colon.
372 auto Tok =
373 llvm::find_if(llvm::reverse(Toks), [](const syntax::Token &Tok) {
374 return Tok.kind() == tok::colon;
375 });
376 assert(Tok != Toks.rend());
377 DeletionRange.setBegin(Tok->location());
378 }
379 }
380 return DeletionRange;
381}
382
383/// Moves definition of a function/method to an appropriate implementation file.
384///
385/// Before:
386/// a.h
387/// void foo() { return; }
388/// a.cc
389/// #include "a.h"
390///
391/// ----------------
392///
393/// After:
394/// a.h
395/// void foo();
396/// a.cc
397/// #include "a.h"
398/// void foo() { return; }
399class DefineOutline : public Tweak {
400public:
401 const char *id() const override;
402
403 bool hidden() const override { return false; }
404 llvm::StringLiteral kind() const override {
405 return CodeAction::REFACTOR_KIND;
406 }
407 std::string title() const override {
408 return "Move function body to out-of-line";
409 }
410
411 bool prepare(const Selection &Sel) override {
412 // Bail out if we are not in a header file.
413 // FIXME: We might want to consider moving method definitions below class
414 // definition even if we are inside a source file.
415 if (!isHeaderFile(FileName: Sel.AST->getSourceManager().getFilename(SpellingLoc: Sel.Cursor),
416 LangOpts: Sel.AST->getLangOpts()))
417 return false;
418
419 Source = getSelectedFunction(SelNode: Sel.ASTSelection.commonAncestor());
420 // Bail out if the selection is not a in-line function definition.
421 if (!Source || !Source->doesThisDeclarationHaveABody() ||
422 Source->isOutOfLine())
423 return false;
424
425 // Bail out if this is a function template or specialization, as their
426 // definitions need to be visible in all including translation units.
427 if (Source->getDescribedFunctionTemplate())
428 return false;
429 if (Source->getTemplateSpecializationInfo())
430 return false;
431
432 if (auto *MD = llvm::dyn_cast<CXXMethodDecl>(Val: Source)) {
433 // Bail out in templated classes, as it is hard to spell the class name,
434 // i.e if the template parameter is unnamed.
435 if (MD->getParent()->isTemplated())
436 return false;
437
438 // The refactoring is meaningless for unnamed classes and definitions
439 // within unnamed namespaces.
440 for (const DeclContext *DC = MD->getParent(); DC; DC = DC->getParent()) {
441 if (auto *ND = llvm::dyn_cast<NamedDecl>(DC)) {
442 if (ND->getDeclName().isEmpty())
443 return false;
444 }
445 }
446 }
447
448 // Note that we don't check whether an implementation file exists or not in
449 // the prepare, since performing disk IO on each prepare request might be
450 // expensive.
451 return true;
452 }
453
454 Expected<Effect> apply(const Selection &Sel) override {
455 const SourceManager &SM = Sel.AST->getSourceManager();
456 auto CCFile = getSourceFile(FileName: Sel.AST->tuPath(), Sel);
457
458 if (!CCFile)
459 return error(Fmt: "Couldn't find a suitable implementation file.");
460 assert(Sel.FS && "FS Must be set in apply");
461 auto Buffer = Sel.FS->getBufferForFile(Name: *CCFile);
462 // FIXME: Maybe we should consider creating the implementation file if it
463 // doesn't exist?
464 if (!Buffer)
465 return llvm::errorCodeToError(EC: Buffer.getError());
466 auto Contents = Buffer->get()->getBuffer();
467 auto InsertionPoint = getInsertionPoint(
468 Contents, Source->getQualifiedNameAsString(), Sel.AST->getLangOpts());
469 if (!InsertionPoint)
470 return InsertionPoint.takeError();
471
472 auto FuncDef = getFunctionSourceCode(
473 Source, InsertionPoint->EnclosingNamespace, Sel.AST->getTokens(),
474 Sel.AST->getHeuristicResolver());
475 if (!FuncDef)
476 return FuncDef.takeError();
477
478 SourceManagerForFile SMFF(*CCFile, Contents);
479 const tooling::Replacement InsertFunctionDef(
480 *CCFile, InsertionPoint->Offset, 0, *FuncDef);
481 auto Effect = Effect::mainFileEdit(
482 SM: SMFF.get(), Replacements: tooling::Replacements(InsertFunctionDef));
483 if (!Effect)
484 return Effect.takeError();
485
486 tooling::Replacements HeaderUpdates(tooling::Replacement(
487 Sel.AST->getSourceManager(),
488 CharSourceRange::getTokenRange(R: *toHalfOpenFileRange(
489 Mgr: SM, LangOpts: Sel.AST->getLangOpts(),
490 R: getDeletionRange(FD: Source, TokBuf: Sel.AST->getTokens()))),
491 ";"));
492
493 if (Source->isInlineSpecified()) {
494 auto DelInline =
495 deleteTokensWithKind(Sel.AST->getTokens(), tok::kw_inline,
496 {Source->getBeginLoc(), Source->getLocation()});
497 if (!DelInline)
498 return DelInline.takeError();
499 HeaderUpdates = HeaderUpdates.merge(Replaces: *DelInline);
500 }
501
502 auto HeaderFE = Effect::fileEdit(SM, FID: SM.getMainFileID(), Replacements: HeaderUpdates);
503 if (!HeaderFE)
504 return HeaderFE.takeError();
505
506 Effect->ApplyEdits.try_emplace(HeaderFE->first,
507 std::move(HeaderFE->second));
508 return std::move(*Effect);
509 }
510
511private:
512 const FunctionDecl *Source = nullptr;
513};
514
515REGISTER_TWEAK(DefineOutline)
516
517} // namespace
518} // namespace clangd
519} // namespace clang
520

source code of clang-tools-extra/clangd/refactor/tweaks/DefineOutline.cpp