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