1 //===--- DefineOutline.cpp ---------------------------------------*- C++-*-===//
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
7 //===----------------------------------------------------------------------===//
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"
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
45 // FIXME: This is shared with define inline, move them to a common header once
46 // we have a place for such.
47 const FunctionDecl
*getSelectedFunction(const SelectionTree::Node
*SelNode
) {
50 const DynTypedNode
&AstNode
= SelNode
->ASTNode
;
51 if (const FunctionDecl
*FD
= AstNode
.get
<FunctionDecl
>())
53 if (AstNode
.get
<CompoundStmt
>() &&
54 SelNode
->Selected
== SelectionTree::Complete
) {
55 if (const SelectionTree::Node
*P
= SelNode
->Parent
)
56 return P
->ASTNode
.get
<FunctionDecl
>();
61 std::optional
<Path
> getSourceFile(llvm::StringRef FileName
,
62 const Tweak::Selection
&Sel
) {
64 if (auto Source
= getCorrespondingHeaderOrSource(FileName
, Sel
.FS
))
66 return getCorrespondingHeaderOrSource(FileName
, *Sel
.AST
, Sel
.Index
);
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.
72 std::optional
<const DeclContext
*>
73 findContextForNS(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();
83 // Otherwise we need to drop any trailing namespaces from CurContext until
85 std::string TargetContextNS
=
86 CurContext
->isNamespace()
87 ? llvm::cast
<NamespaceDecl
>(CurContext
)->getQualifiedNameAsString()
89 TargetContextNS
.append("::");
91 llvm::StringRef
CurrentContextNS(TargetContextNS
);
92 // If TargetNS is not a prefix of CurrentContext, there's no way to reach
94 if (!CurrentContextNS
.starts_with(TargetNS
))
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 CurrentContextNS
.drop_back(2).rfind("::") + 2);
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.
110 llvm::Expected
<std::string
>
111 getFunctionSourceAfterReplacements(const FunctionDecl
*FD
,
112 const tooling::Replacements
&Replacements
,
113 bool TargetFileIsHeader
) {
114 const auto &SM
= FD
->getASTContext().getSourceManager();
115 auto OrigFuncRange
= toHalfOpenFileRange(
116 SM
, FD
->getASTContext().getLangOpts(), FD
->getSourceRange());
118 return error("Couldn't get range for function.");
120 // Get new begin and end positions for the qualified function definition.
121 unsigned FuncBegin
= SM
.getFileOffset(OrigFuncRange
->getBegin());
122 unsigned FuncEnd
= Replacements
.getShiftedCodePosition(
123 SM
.getFileOffset(OrigFuncRange
->getEnd()));
125 // Trim the result to function definition.
126 auto QualifiedFunc
= tooling::applyAllReplacements(
127 SM
.getBufferData(SM
.getMainFileID()), Replacements
);
129 return QualifiedFunc
.takeError();
131 auto Source
= QualifiedFunc
->substr(FuncBegin
, FuncEnd
- FuncBegin
+ 1);
132 std::string TemplatePrefix
;
133 auto AddToTemplatePrefixIfApplicable
= [&](const Decl
*D
) {
134 const TemplateParameterList
*Params
= D
->getDescribedTemplateParams();
137 for (Decl
*P
: *Params
) {
138 if (auto *TTP
= dyn_cast
<TemplateTypeParmDecl
>(P
))
139 TTP
->removeDefaultArgument();
140 else if (auto *NTTP
= dyn_cast
<NonTypeTemplateParmDecl
>(P
))
141 NTTP
->removeDefaultArgument();
142 else if (auto *TTPD
= dyn_cast
<TemplateTemplateParmDecl
>(P
))
143 TTPD
->removeDefaultArgument();
146 llvm::raw_string_ostream
Stream(S
);
147 Params
->print(Stream
, FD
->getASTContext());
149 *S
.rbegin() = '\n'; // Replace space with newline
150 TemplatePrefix
.insert(0, S
);
152 AddToTemplatePrefixIfApplicable(FD
);
153 if (auto *MD
= llvm::dyn_cast
<CXXMethodDecl
>(FD
)) {
154 for (const CXXRecordDecl
*Parent
= MD
->getParent(); Parent
;
156 llvm::dyn_cast_or_null
<const CXXRecordDecl
>(Parent
->getParent())) {
157 AddToTemplatePrefixIfApplicable(Parent
);
161 if (TargetFileIsHeader
)
162 Source
.insert(0, "inline ");
163 if (!TemplatePrefix
.empty())
164 Source
.insert(0, TemplatePrefix
);
168 // Returns replacements to delete tokens with kind `Kind` in the range
169 // `FromRange`. Removes matching instances of given token preceeding the
170 // function defition.
171 llvm::Expected
<tooling::Replacements
>
172 deleteTokensWithKind(const syntax::TokenBuffer
&TokBuf
, tok::TokenKind Kind
,
173 SourceRange FromRange
) {
174 tooling::Replacements DelKeywordCleanups
;
175 llvm::Error Errors
= llvm::Error::success();
176 bool FoundAny
= false;
177 for (const auto &Tok
: TokBuf
.expandedTokens(FromRange
)) {
178 if (Tok
.kind() != Kind
)
181 auto Spelling
= TokBuf
.spelledForExpanded(llvm::ArrayRef(Tok
));
183 Errors
= llvm::joinErrors(
185 error("define outline: couldn't remove `{0}` keyword.",
186 tok::getKeywordSpelling(Kind
)));
189 auto &SM
= TokBuf
.sourceManager();
190 CharSourceRange DelRange
=
191 syntax::Token::range(SM
, Spelling
->front(), Spelling
->back())
194 DelKeywordCleanups
.add(tooling::Replacement(SM
, DelRange
, "")))
195 Errors
= llvm::joinErrors(std::move(Errors
), std::move(Err
));
198 Errors
= llvm::joinErrors(
200 error("define outline: couldn't find `{0}` keyword to remove.",
201 tok::getKeywordSpelling(Kind
)));
205 return std::move(Errors
);
206 return DelKeywordCleanups
;
209 // Creates a modified version of function definition that can be inserted at a
210 // different location, qualifies return value and function name to achieve that.
211 // Contains function signature, except defaulted parameter arguments, body and
212 // template parameters if applicable. No need to qualify parameters, as they are
213 // looked up in the context containing the function/method.
214 // FIXME: Drop attributes in function signature.
215 llvm::Expected
<std::string
>
216 getFunctionSourceCode(const FunctionDecl
*FD
, const DeclContext
*TargetContext
,
217 const syntax::TokenBuffer
&TokBuf
,
218 const HeuristicResolver
*Resolver
,
219 bool TargetFileIsHeader
) {
220 auto &AST
= FD
->getASTContext();
221 auto &SM
= AST
.getSourceManager();
223 llvm::Error Errors
= llvm::Error::success();
224 tooling::Replacements DeclarationCleanups
;
226 // Finds the first unqualified name in function return type and name, then
227 // qualifies those to be valid in TargetContext.
228 findExplicitReferences(
230 [&](ReferenceLoc Ref
) {
231 // It is enough to qualify the first qualifier, so skip references with
232 // a qualifier. Also we can't do much if there are no targets or name is
233 // inside a macro body.
234 if (Ref
.Qualifier
|| Ref
.Targets
.empty() || Ref
.NameLoc
.isMacroID())
236 // Only qualify return type and function name.
237 if (Ref
.NameLoc
!= FD
->getReturnTypeSourceRange().getBegin() &&
238 Ref
.NameLoc
!= FD
->getLocation())
241 for (const NamedDecl
*ND
: Ref
.Targets
) {
242 if (ND
->getKind() == Decl::TemplateTypeParm
)
244 if (ND
->getDeclContext() != Ref
.Targets
.front()->getDeclContext()) {
245 elog("Targets from multiple contexts: {0}, {1}",
246 printQualifiedName(*Ref
.Targets
.front()),
247 printQualifiedName(*ND
));
251 const NamedDecl
*ND
= Ref
.Targets
.front();
252 std::string Qualifier
=
253 getQualification(AST
, TargetContext
,
254 SM
.getLocForStartOfFile(SM
.getMainFileID()), ND
);
255 if (ND
->getDeclContext()->isDependentContext() &&
256 llvm::isa
<TypeDecl
>(ND
)) {
257 Qualifier
.insert(0, "typename ");
259 if (auto Err
= DeclarationCleanups
.add(
260 tooling::Replacement(SM
, Ref
.NameLoc
, 0, Qualifier
)))
261 Errors
= llvm::joinErrors(std::move(Errors
), std::move(Err
));
265 // findExplicitReferences doesn't provide references to
266 // constructor/destructors, it only provides references to type names inside
268 // this works for constructors, but doesn't work for destructor as type name
269 // doesn't cover leading `~`, so handle it specially.
270 if (const auto *Destructor
= llvm::dyn_cast
<CXXDestructorDecl
>(FD
)) {
271 if (auto Err
= DeclarationCleanups
.add(tooling::Replacement(
272 SM
, Destructor
->getLocation(), 0,
273 getQualification(AST
, TargetContext
,
274 SM
.getLocForStartOfFile(SM
.getMainFileID()),
276 Errors
= llvm::joinErrors(std::move(Errors
), std::move(Err
));
279 // Get rid of default arguments, since they should not be specified in
280 // out-of-line definition.
281 for (const auto *PVD
: FD
->parameters()) {
282 if (!PVD
->hasDefaultArg())
284 // Deletion range spans the initializer, usually excluding the `=`.
285 auto DelRange
= CharSourceRange::getTokenRange(PVD
->getDefaultArgRange());
286 // Get all tokens before the default argument.
287 auto Tokens
= TokBuf
.expandedTokens(PVD
->getSourceRange())
288 .take_while([&SM
, &DelRange
](const syntax::Token
&Tok
) {
289 return SM
.isBeforeInTranslationUnit(
290 Tok
.location(), DelRange
.getBegin());
292 if (TokBuf
.expandedTokens(DelRange
.getAsRange()).front().kind() !=
294 // Find the last `=` if it isn't included in the initializer, and update
295 // the DelRange to include it.
297 llvm::find_if(llvm::reverse(Tokens
), [](const syntax::Token
&Tok
) {
298 return Tok
.kind() == tok::equal
;
300 assert(Tok
!= Tokens
.rend());
301 DelRange
.setBegin(Tok
->location());
304 DeclarationCleanups
.add(tooling::Replacement(SM
, DelRange
, "")))
305 Errors
= llvm::joinErrors(std::move(Errors
), std::move(Err
));
308 auto DelAttr
= [&](const Attr
*A
) {
312 TokBuf
.spelledForExpanded(TokBuf
.expandedTokens(A
->getRange()));
313 assert(A
->getLocation().isValid());
314 if (!AttrTokens
|| AttrTokens
->empty()) {
315 Errors
= llvm::joinErrors(
316 std::move(Errors
), error("define outline: Can't move out of line as "
317 "function has a macro `{0}` specifier.",
321 CharSourceRange DelRange
=
322 syntax::Token::range(SM
, AttrTokens
->front(), AttrTokens
->back())
325 DeclarationCleanups
.add(tooling::Replacement(SM
, DelRange
, "")))
326 Errors
= llvm::joinErrors(std::move(Errors
), std::move(Err
));
329 DelAttr(FD
->getAttr
<OverrideAttr
>());
330 DelAttr(FD
->getAttr
<FinalAttr
>());
332 auto DelKeyword
= [&](tok::TokenKind Kind
, SourceRange FromRange
) {
333 auto DelKeywords
= deleteTokensWithKind(TokBuf
, Kind
, FromRange
);
335 Errors
= llvm::joinErrors(std::move(Errors
), DelKeywords
.takeError());
338 DeclarationCleanups
= DeclarationCleanups
.merge(*DelKeywords
);
341 if (FD
->isInlineSpecified())
342 DelKeyword(tok::kw_inline
, {FD
->getBeginLoc(), FD
->getLocation()});
343 if (const auto *MD
= dyn_cast
<CXXMethodDecl
>(FD
)) {
344 if (MD
->isVirtualAsWritten())
345 DelKeyword(tok::kw_virtual
, {FD
->getBeginLoc(), FD
->getLocation()});
347 DelKeyword(tok::kw_static
, {FD
->getBeginLoc(), FD
->getLocation()});
349 if (const auto *CD
= dyn_cast
<CXXConstructorDecl
>(FD
)) {
350 if (CD
->isExplicit())
351 DelKeyword(tok::kw_explicit
, {FD
->getBeginLoc(), FD
->getLocation()});
355 return std::move(Errors
);
356 return getFunctionSourceAfterReplacements(FD
, DeclarationCleanups
,
360 struct InsertionPoint
{
361 const DeclContext
*EnclosingNamespace
= nullptr;
365 // Returns the range that should be deleted from declaration, which always
366 // contains function body. In addition to that it might contain constructor
368 SourceRange
getDeletionRange(const FunctionDecl
*FD
,
369 const syntax::TokenBuffer
&TokBuf
) {
370 auto DeletionRange
= FD
->getBody()->getSourceRange();
371 if (auto *CD
= llvm::dyn_cast
<CXXConstructorDecl
>(FD
)) {
372 // AST doesn't contain the location for ":" in ctor initializers. Therefore
373 // we find it by finding the first ":" before the first ctor initializer.
374 SourceLocation InitStart
;
375 // Find the first initializer.
376 for (const auto *CInit
: CD
->inits()) {
377 // SourceOrder is -1 for implicit initializers.
378 if (CInit
->getSourceOrder() != 0)
380 InitStart
= CInit
->getSourceLocation();
383 if (InitStart
.isValid()) {
384 auto Toks
= TokBuf
.expandedTokens(CD
->getSourceRange());
385 // Drop any tokens after the initializer.
386 Toks
= Toks
.take_while([&TokBuf
, &InitStart
](const syntax::Token
&Tok
) {
387 return TokBuf
.sourceManager().isBeforeInTranslationUnit(Tok
.location(),
390 // Look for the first colon.
392 llvm::find_if(llvm::reverse(Toks
), [](const syntax::Token
&Tok
) {
393 return Tok
.kind() == tok::colon
;
395 assert(Tok
!= Toks
.rend());
396 DeletionRange
.setBegin(Tok
->location());
399 return DeletionRange
;
402 /// Moves definition of a function/method to an appropriate implementation file.
406 /// void foo() { return; }
417 /// void foo() { return; }
418 class DefineOutline
: public Tweak
{
420 const char *id() const override
;
422 bool hidden() const override
{ return false; }
423 llvm::StringLiteral
kind() const override
{
424 return CodeAction::REFACTOR_KIND
;
426 std::string
title() const override
{
427 return "Move function body to out-of-line";
430 bool prepare(const Selection
&Sel
) override
{
431 SameFile
= !isHeaderFile(Sel
.AST
->tuPath(), Sel
.AST
->getLangOpts());
432 Source
= getSelectedFunction(Sel
.ASTSelection
.commonAncestor());
434 // Bail out if the selection is not a in-line function definition.
435 if (!Source
|| !Source
->doesThisDeclarationHaveABody() ||
436 Source
->isOutOfLine())
439 // Bail out if this is a function template specialization, as their
440 // definitions need to be visible in all including translation units.
441 if (Source
->getTemplateSpecializationInfo())
444 auto *MD
= llvm::dyn_cast
<CXXMethodDecl
>(Source
);
446 if (Source
->getDescribedFunctionTemplate())
448 // Can't outline free-standing functions in the same file.
452 for (const CXXRecordDecl
*Parent
= MD
->getParent(); Parent
;
454 llvm::dyn_cast_or_null
<const CXXRecordDecl
>(Parent
->getParent())) {
455 if (const TemplateParameterList
*Params
=
456 Parent
->getDescribedTemplateParams()) {
458 // Class template member functions must be defined in the
462 // Bail out if the template parameter is unnamed.
463 for (NamedDecl
*P
: *Params
) {
464 if (!P
->getIdentifier())
470 // Function templates must be defined in the same file.
471 if (MD
->getDescribedTemplate())
474 // The refactoring is meaningless for unnamed classes and namespaces,
475 // unless we're outlining in the same file
476 for (const DeclContext
*DC
= MD
->getParent(); DC
; DC
= DC
->getParent()) {
477 if (auto *ND
= llvm::dyn_cast
<NamedDecl
>(DC
)) {
478 if (ND
->getDeclName().isEmpty() &&
479 (!SameFile
|| !llvm::dyn_cast
<NamespaceDecl
>(ND
)))
484 // Note that we don't check whether an implementation file exists or not in
485 // the prepare, since performing disk IO on each prepare request might be
490 Expected
<Effect
> apply(const Selection
&Sel
) override
{
491 const SourceManager
&SM
= Sel
.AST
->getSourceManager();
492 auto CCFile
= SameFile
? Sel
.AST
->tuPath().str()
493 : getSourceFile(Sel
.AST
->tuPath(), Sel
);
495 return error("Couldn't find a suitable implementation file.");
496 assert(Sel
.FS
&& "FS Must be set in apply");
497 auto Buffer
= Sel
.FS
->getBufferForFile(*CCFile
);
498 // FIXME: Maybe we should consider creating the implementation file if it
501 return llvm::errorCodeToError(Buffer
.getError());
502 auto Contents
= Buffer
->get()->getBuffer();
503 auto InsertionPoint
= getInsertionPoint(Contents
, Sel
);
505 return InsertionPoint
.takeError();
507 auto FuncDef
= getFunctionSourceCode(
508 Source
, InsertionPoint
->EnclosingNamespace
, Sel
.AST
->getTokens(),
509 Sel
.AST
->getHeuristicResolver(),
510 SameFile
&& isHeaderFile(Sel
.AST
->tuPath(), Sel
.AST
->getLangOpts()));
512 return FuncDef
.takeError();
514 SourceManagerForFile
SMFF(*CCFile
, Contents
);
515 const tooling::Replacement
InsertFunctionDef(
516 *CCFile
, InsertionPoint
->Offset
, 0, *FuncDef
);
517 auto Effect
= Effect::mainFileEdit(
518 SMFF
.get(), tooling::Replacements(InsertFunctionDef
));
520 return Effect
.takeError();
522 tooling::Replacements
HeaderUpdates(tooling::Replacement(
523 Sel
.AST
->getSourceManager(),
524 CharSourceRange::getTokenRange(*toHalfOpenFileRange(
525 SM
, Sel
.AST
->getLangOpts(),
526 getDeletionRange(Source
, Sel
.AST
->getTokens()))),
529 if (Source
->isInlineSpecified()) {
531 deleteTokensWithKind(Sel
.AST
->getTokens(), tok::kw_inline
,
532 {Source
->getBeginLoc(), Source
->getLocation()});
534 return DelInline
.takeError();
535 HeaderUpdates
= HeaderUpdates
.merge(*DelInline
);
539 tooling::Replacements
&R
= Effect
->ApplyEdits
[*CCFile
].Replacements
;
540 R
= R
.merge(HeaderUpdates
);
542 auto HeaderFE
= Effect::fileEdit(SM
, SM
.getMainFileID(), HeaderUpdates
);
544 return HeaderFE
.takeError();
545 Effect
->ApplyEdits
.try_emplace(HeaderFE
->first
,
546 std::move(HeaderFE
->second
));
548 return std::move(*Effect
);
551 // Returns the most natural insertion point for \p QualifiedName in \p
552 // Contents. This currently cares about only the namespace proximity, but in
553 // feature it should also try to follow ordering of declarations. For example,
554 // if decls come in order `foo, bar, baz` then this function should return
555 // some point between foo and baz for inserting bar.
556 // FIXME: The selection can be made smarter by looking at the definition
557 // locations for adjacent decls to Source. Unfortunately pseudo parsing in
558 // getEligibleRegions only knows about namespace begin/end events so we
559 // can't match function start/end positions yet.
560 llvm::Expected
<InsertionPoint
> getInsertionPoint(llvm::StringRef Contents
,
561 const Selection
&Sel
) {
562 // If the definition goes to the same file and there is a namespace,
563 // we should (and, in the case of anonymous namespaces, need to)
564 // put the definition into the original namespace block.
566 auto *Klass
= Source
->getDeclContext()->getOuterLexicalRecordContext();
568 return error("moving to same file not supported for free functions");
569 const SourceLocation EndLoc
= Klass
->getBraceRange().getEnd();
570 const auto &TokBuf
= Sel
.AST
->getTokens();
571 auto Tokens
= TokBuf
.expandedTokens();
572 auto It
= llvm::lower_bound(
573 Tokens
, EndLoc
, [](const syntax::Token
&Tok
, SourceLocation EndLoc
) {
574 return Tok
.location() < EndLoc
;
576 while (It
!= Tokens
.end()) {
577 if (It
->kind() != tok::semi
) {
581 unsigned Offset
= Sel
.AST
->getSourceManager()
582 .getDecomposedLoc(It
->endLocation())
584 return InsertionPoint
{Klass
->getEnclosingNamespaceContext(), Offset
};
587 "failed to determine insertion location: no end of class found");
590 auto Region
= getEligiblePoints(
591 Contents
, Source
->getQualifiedNameAsString(), Sel
.AST
->getLangOpts());
593 assert(!Region
.EligiblePoints
.empty());
594 auto Offset
= positionToOffset(Contents
, Region
.EligiblePoints
.back());
596 return Offset
.takeError();
599 findContextForNS(Region
.EnclosingNamespace
, Source
->getDeclContext());
601 return error("define outline: couldn't find a context for target");
603 return InsertionPoint
{*TargetContext
, *Offset
};
607 const FunctionDecl
*Source
= nullptr;
608 bool SameFile
= false;
611 REGISTER_TWEAK(DefineOutline
)
614 } // namespace clangd