| 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 | |
| 38 | namespace clang { |
| 39 | namespace clangd { |
| 40 | namespace { |
| 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. |
| 47 | const 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 | |
| 61 | std::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. |
| 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(); |
| 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. |
| 110 | llvm::Expected<std::string> |
| 111 | getFunctionSourceAfterReplacements(const FunctionDecl *FD, |
| 112 | const tooling::Replacements &Replacements, |
| 113 | bool ) { |
| 114 | const auto &SM = FD->getASTContext().getSourceManager(); |
| 115 | auto OrigFuncRange = toHalfOpenFileRange( |
| 116 | SM, FD->getASTContext().getLangOpts(), FD->getSourceRange()); |
| 117 | if (!OrigFuncRange) |
| 118 | return error(Fmt: "Couldn't get range for function." ); |
| 119 | |
| 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 | Position: SM.getFileOffset(OrigFuncRange->getEnd())); |
| 124 | |
| 125 | // Trim the result to function definition. |
| 126 | auto QualifiedFunc = tooling::applyAllReplacements( |
| 127 | SM.getBufferData(SM.getMainFileID()), Replacements); |
| 128 | if (!QualifiedFunc) |
| 129 | return QualifiedFunc.takeError(); |
| 130 | |
| 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(); |
| 135 | if (!Params) |
| 136 | return; |
| 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(); |
| 144 | } |
| 145 | std::string S; |
| 146 | llvm::raw_string_ostream Stream(S); |
| 147 | Params->print(Stream, FD->getASTContext()); |
| 148 | if (!S.empty()) |
| 149 | *S.rbegin() = '\n'; // Replace space with newline |
| 150 | TemplatePrefix.insert(pos1: 0, str: S); |
| 151 | }; |
| 152 | AddToTemplatePrefixIfApplicable(FD); |
| 153 | if (auto *MD = llvm::dyn_cast<CXXMethodDecl>(Val: FD)) { |
| 154 | for (const CXXRecordDecl *Parent = MD->getParent(); Parent; |
| 155 | Parent = |
| 156 | llvm::dyn_cast_or_null<const CXXRecordDecl>(Parent->getParent())) { |
| 157 | AddToTemplatePrefixIfApplicable(Parent); |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | if (TargetFileIsHeader) |
| 162 | Source.insert(0, "inline " ); |
| 163 | if (!TemplatePrefix.empty()) |
| 164 | Source.insert(0, TemplatePrefix); |
| 165 | return Source; |
| 166 | } |
| 167 | |
| 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(R: FromRange)) { |
| 178 | if (Tok.kind() != Kind) |
| 179 | continue; |
| 180 | FoundAny = true; |
| 181 | auto Spelling = TokBuf.spelledForExpanded(Expanded: llvm::ArrayRef(Tok)); |
| 182 | if (!Spelling) { |
| 183 | Errors = llvm::joinErrors( |
| 184 | E1: std::move(Errors), |
| 185 | E2: error(Fmt: "define outline: couldn't remove `{0}` keyword." , |
| 186 | Vals: tok::getKeywordSpelling(Kind))); |
| 187 | break; |
| 188 | } |
| 189 | auto &SM = TokBuf.sourceManager(); |
| 190 | CharSourceRange DelRange = |
| 191 | syntax::Token::range(SM, First: Spelling->front(), Last: Spelling->back()) |
| 192 | .toCharRange(SM); |
| 193 | if (auto Err = |
| 194 | DelKeywordCleanups.add(R: tooling::Replacement(SM, DelRange, "" ))) |
| 195 | Errors = llvm::joinErrors(E1: std::move(Errors), E2: std::move(Err)); |
| 196 | } |
| 197 | if (!FoundAny) { |
| 198 | Errors = llvm::joinErrors( |
| 199 | E1: std::move(Errors), |
| 200 | E2: error(Fmt: "define outline: couldn't find `{0}` keyword to remove." , |
| 201 | Vals: tok::getKeywordSpelling(Kind))); |
| 202 | } |
| 203 | |
| 204 | if (Errors) |
| 205 | return std::move(Errors); |
| 206 | return DelKeywordCleanups; |
| 207 | } |
| 208 | |
| 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 ) { |
| 220 | auto &AST = FD->getASTContext(); |
| 221 | auto &SM = AST.getSourceManager(); |
| 222 | |
| 223 | llvm::Error Errors = llvm::Error::success(); |
| 224 | tooling::Replacements DeclarationCleanups; |
| 225 | |
| 226 | // Finds the first unqualified name in function return type and name, then |
| 227 | // qualifies those to be valid in TargetContext. |
| 228 | findExplicitReferences( |
| 229 | FD, |
| 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()) |
| 235 | return; |
| 236 | // Only qualify return type and function name. |
| 237 | if (Ref.NameLoc != FD->getReturnTypeSourceRange().getBegin() && |
| 238 | Ref.NameLoc != FD->getLocation()) |
| 239 | return; |
| 240 | |
| 241 | for (const NamedDecl *ND : Ref.Targets) { |
| 242 | if (ND->getKind() == Decl::TemplateTypeParm) |
| 243 | return; |
| 244 | if (ND->getDeclContext() != Ref.Targets.front()->getDeclContext()) { |
| 245 | elog(Fmt: "Targets from multiple contexts: {0}, {1}" , |
| 246 | Vals: printQualifiedName(ND: *Ref.Targets.front()), |
| 247 | Vals: printQualifiedName(ND: *ND)); |
| 248 | return; |
| 249 | } |
| 250 | } |
| 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>(Val: ND)) { |
| 257 | Qualifier.insert(pos: 0, s: "typename " ); |
| 258 | } |
| 259 | if (auto Err = DeclarationCleanups.add( |
| 260 | tooling::Replacement(SM, Ref.NameLoc, 0, Qualifier))) |
| 261 | Errors = llvm::joinErrors(E1: std::move(Errors), E2: std::move(Err)); |
| 262 | }, |
| 263 | Resolver); |
| 264 | |
| 265 | // findExplicitReferences doesn't provide references to |
| 266 | // constructor/destructors, it only provides references to type names inside |
| 267 | // them. |
| 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>(Val: FD)) { |
| 271 | if (auto Err = DeclarationCleanups.add(tooling::Replacement( |
| 272 | SM, Destructor->getLocation(), 0, |
| 273 | getQualification(AST, TargetContext, |
| 274 | SM.getLocForStartOfFile(SM.getMainFileID()), |
| 275 | Destructor)))) |
| 276 | Errors = llvm::joinErrors(E1: std::move(Errors), E2: std::move(Err)); |
| 277 | } |
| 278 | |
| 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()) |
| 283 | continue; |
| 284 | // Deletion range spans the initializer, usually excluding the `=`. |
| 285 | auto DelRange = CharSourceRange::getTokenRange(R: PVD->getDefaultArgRange()); |
| 286 | // Get all tokens before the default argument. |
| 287 | auto Tokens = TokBuf.expandedTokens(R: PVD->getSourceRange()) |
| 288 | .take_while(Pred: [&SM, &DelRange](const syntax::Token &Tok) { |
| 289 | return SM.isBeforeInTranslationUnit( |
| 290 | Tok.location(), DelRange.getBegin()); |
| 291 | }); |
| 292 | if (TokBuf.expandedTokens(R: DelRange.getAsRange()).front().kind() != |
| 293 | tok::equal) { |
| 294 | // Find the last `=` if it isn't included in the initializer, and update |
| 295 | // the DelRange to include it. |
| 296 | auto Tok = |
| 297 | llvm::find_if(Range: llvm::reverse(C&: Tokens), P: [](const syntax::Token &Tok) { |
| 298 | return Tok.kind() == tok::equal; |
| 299 | }); |
| 300 | assert(Tok != Tokens.rend()); |
| 301 | DelRange.setBegin(Tok->location()); |
| 302 | } |
| 303 | if (auto Err = |
| 304 | DeclarationCleanups.add(tooling::Replacement(SM, DelRange, "" ))) |
| 305 | Errors = llvm::joinErrors(E1: std::move(Errors), E2: std::move(Err)); |
| 306 | } |
| 307 | |
| 308 | auto DelAttr = [&](const Attr *A) { |
| 309 | if (!A) |
| 310 | return; |
| 311 | auto AttrTokens = |
| 312 | TokBuf.spelledForExpanded(Expanded: TokBuf.expandedTokens(R: A->getRange())); |
| 313 | assert(A->getLocation().isValid()); |
| 314 | if (!AttrTokens || AttrTokens->empty()) { |
| 315 | Errors = llvm::joinErrors( |
| 316 | E1: std::move(Errors), E2: error(Fmt: "define outline: Can't move out of line as " |
| 317 | "function has a macro `{0}` specifier." , |
| 318 | Vals: A->getSpelling())); |
| 319 | return; |
| 320 | } |
| 321 | CharSourceRange DelRange = |
| 322 | syntax::Token::range(SM, AttrTokens->front(), AttrTokens->back()) |
| 323 | .toCharRange(SM); |
| 324 | if (auto Err = |
| 325 | DeclarationCleanups.add(tooling::Replacement(SM, DelRange, "" ))) |
| 326 | Errors = llvm::joinErrors(E1: std::move(Errors), E2: std::move(Err)); |
| 327 | }; |
| 328 | |
| 329 | DelAttr(FD->getAttr<OverrideAttr>()); |
| 330 | DelAttr(FD->getAttr<FinalAttr>()); |
| 331 | |
| 332 | auto DelKeyword = [&](tok::TokenKind Kind, SourceRange FromRange) { |
| 333 | auto DelKeywords = deleteTokensWithKind(TokBuf, Kind, FromRange); |
| 334 | if (!DelKeywords) { |
| 335 | Errors = llvm::joinErrors(E1: std::move(Errors), E2: DelKeywords.takeError()); |
| 336 | return; |
| 337 | } |
| 338 | DeclarationCleanups = DeclarationCleanups.merge(Replaces: *DelKeywords); |
| 339 | }; |
| 340 | |
| 341 | if (FD->isInlineSpecified()) |
| 342 | DelKeyword(tok::kw_inline, {FD->getBeginLoc(), FD->getLocation()}); |
| 343 | if (const auto *MD = dyn_cast<CXXMethodDecl>(Val: FD)) { |
| 344 | if (MD->isVirtualAsWritten()) |
| 345 | DelKeyword(tok::kw_virtual, {FD->getBeginLoc(), FD->getLocation()}); |
| 346 | if (MD->isStatic()) |
| 347 | DelKeyword(tok::kw_static, {FD->getBeginLoc(), FD->getLocation()}); |
| 348 | } |
| 349 | if (const auto *CD = dyn_cast<CXXConstructorDecl>(Val: FD)) { |
| 350 | if (CD->isExplicit()) |
| 351 | DelKeyword(tok::kw_explicit, {FD->getBeginLoc(), FD->getLocation()}); |
| 352 | } |
| 353 | |
| 354 | if (Errors) |
| 355 | return std::move(Errors); |
| 356 | return getFunctionSourceAfterReplacements(FD, Replacements: DeclarationCleanups, |
| 357 | TargetFileIsHeader); |
| 358 | } |
| 359 | |
| 360 | struct InsertionPoint { |
| 361 | const DeclContext *EnclosingNamespace = nullptr; |
| 362 | size_t Offset; |
| 363 | }; |
| 364 | |
| 365 | // Returns the range that should be deleted from declaration, which always |
| 366 | // contains function body. In addition to that it might contain constructor |
| 367 | // initializers. |
| 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>(Val: 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) |
| 379 | continue; |
| 380 | InitStart = CInit->getSourceLocation(); |
| 381 | break; |
| 382 | } |
| 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(LHS: Tok.location(), |
| 388 | RHS: InitStart); |
| 389 | }); |
| 390 | // Look for the first colon. |
| 391 | auto Tok = |
| 392 | llvm::find_if(llvm::reverse(Toks), [](const syntax::Token &Tok) { |
| 393 | return Tok.kind() == tok::colon; |
| 394 | }); |
| 395 | assert(Tok != Toks.rend()); |
| 396 | DeletionRange.setBegin(Tok->location()); |
| 397 | } |
| 398 | } |
| 399 | return DeletionRange; |
| 400 | } |
| 401 | |
| 402 | /// Moves definition of a function/method to an appropriate implementation file. |
| 403 | /// |
| 404 | /// Before: |
| 405 | /// a.h |
| 406 | /// void foo() { return; } |
| 407 | /// a.cc |
| 408 | /// #include "a.h" |
| 409 | /// |
| 410 | /// ---------------- |
| 411 | /// |
| 412 | /// After: |
| 413 | /// a.h |
| 414 | /// void foo(); |
| 415 | /// a.cc |
| 416 | /// #include "a.h" |
| 417 | /// void foo() { return; } |
| 418 | class DefineOutline : public Tweak { |
| 419 | public: |
| 420 | const char *id() const override; |
| 421 | |
| 422 | bool hidden() const override { return false; } |
| 423 | llvm::StringLiteral kind() const override { |
| 424 | return CodeAction::REFACTOR_KIND; |
| 425 | } |
| 426 | std::string title() const override { |
| 427 | return "Move function body to out-of-line" ; |
| 428 | } |
| 429 | |
| 430 | bool prepare(const Selection &Sel) override { |
| 431 | SameFile = !isHeaderFile(FileName: Sel.AST->tuPath(), LangOpts: Sel.AST->getLangOpts()); |
| 432 | Source = getSelectedFunction(SelNode: Sel.ASTSelection.commonAncestor()); |
| 433 | |
| 434 | // Bail out if the selection is not a in-line function definition. |
| 435 | if (!Source || !Source->doesThisDeclarationHaveABody() || |
| 436 | Source->isOutOfLine()) |
| 437 | return false; |
| 438 | |
| 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()) |
| 442 | return false; |
| 443 | |
| 444 | auto *MD = llvm::dyn_cast<CXXMethodDecl>(Val: Source); |
| 445 | if (!MD) { |
| 446 | if (Source->getDescribedFunctionTemplate()) |
| 447 | return false; |
| 448 | // Can't outline free-standing functions in the same file. |
| 449 | return !SameFile; |
| 450 | } |
| 451 | |
| 452 | for (const CXXRecordDecl *Parent = MD->getParent(); Parent; |
| 453 | Parent = |
| 454 | llvm::dyn_cast_or_null<const CXXRecordDecl>(Parent->getParent())) { |
| 455 | if (const TemplateParameterList *Params = |
| 456 | Parent->getDescribedTemplateParams()) { |
| 457 | |
| 458 | // Class template member functions must be defined in the |
| 459 | // same file. |
| 460 | SameFile = true; |
| 461 | |
| 462 | // Bail out if the template parameter is unnamed. |
| 463 | for (NamedDecl *P : *Params) { |
| 464 | if (!P->getIdentifier()) |
| 465 | return false; |
| 466 | } |
| 467 | } |
| 468 | } |
| 469 | |
| 470 | // Function templates must be defined in the same file. |
| 471 | if (MD->getDescribedTemplate()) |
| 472 | SameFile = true; |
| 473 | |
| 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))) |
| 480 | return false; |
| 481 | } |
| 482 | } |
| 483 | |
| 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 |
| 486 | // expensive. |
| 487 | return true; |
| 488 | } |
| 489 | |
| 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(FileName: Sel.AST->tuPath(), Sel); |
| 494 | if (!CCFile) |
| 495 | return error(Fmt: "Couldn't find a suitable implementation file." ); |
| 496 | assert(Sel.FS && "FS Must be set in apply" ); |
| 497 | auto Buffer = Sel.FS->getBufferForFile(Name: *CCFile); |
| 498 | // FIXME: Maybe we should consider creating the implementation file if it |
| 499 | // doesn't exist? |
| 500 | if (!Buffer) |
| 501 | return llvm::errorCodeToError(EC: Buffer.getError()); |
| 502 | auto Contents = Buffer->get()->getBuffer(); |
| 503 | auto InsertionPoint = getInsertionPoint(Contents, Sel); |
| 504 | if (!InsertionPoint) |
| 505 | return InsertionPoint.takeError(); |
| 506 | |
| 507 | auto FuncDef = getFunctionSourceCode( |
| 508 | FD: Source, TargetContext: InsertionPoint->EnclosingNamespace, TokBuf: Sel.AST->getTokens(), |
| 509 | Resolver: Sel.AST->getHeuristicResolver(), |
| 510 | TargetFileIsHeader: SameFile && isHeaderFile(FileName: Sel.AST->tuPath(), LangOpts: Sel.AST->getLangOpts())); |
| 511 | if (!FuncDef) |
| 512 | return FuncDef.takeError(); |
| 513 | |
| 514 | SourceManagerForFile SMFF(*CCFile, Contents); |
| 515 | const tooling::Replacement InsertFunctionDef( |
| 516 | *CCFile, InsertionPoint->Offset, 0, *FuncDef); |
| 517 | auto Effect = Effect::mainFileEdit( |
| 518 | SM: SMFF.get(), Replacements: tooling::Replacements(InsertFunctionDef)); |
| 519 | if (!Effect) |
| 520 | return Effect.takeError(); |
| 521 | |
| 522 | tooling::Replacements (tooling::Replacement( |
| 523 | Sel.AST->getSourceManager(), |
| 524 | CharSourceRange::getTokenRange(R: *toHalfOpenFileRange( |
| 525 | Mgr: SM, LangOpts: Sel.AST->getLangOpts(), |
| 526 | R: getDeletionRange(FD: Source, TokBuf: Sel.AST->getTokens()))), |
| 527 | ";" )); |
| 528 | |
| 529 | if (Source->isInlineSpecified()) { |
| 530 | auto DelInline = |
| 531 | deleteTokensWithKind(Sel.AST->getTokens(), tok::kw_inline, |
| 532 | {Source->getBeginLoc(), Source->getLocation()}); |
| 533 | if (!DelInline) |
| 534 | return DelInline.takeError(); |
| 535 | HeaderUpdates = HeaderUpdates.merge(Replaces: *DelInline); |
| 536 | } |
| 537 | |
| 538 | if (SameFile) { |
| 539 | tooling::Replacements &R = Effect->ApplyEdits[*CCFile].Replacements; |
| 540 | R = R.merge(Replaces: HeaderUpdates); |
| 541 | } else { |
| 542 | auto = Effect::fileEdit(SM, FID: SM.getMainFileID(), Replacements: HeaderUpdates); |
| 543 | if (!HeaderFE) |
| 544 | return HeaderFE.takeError(); |
| 545 | Effect->ApplyEdits.try_emplace(Key: HeaderFE->first, |
| 546 | Args: std::move(HeaderFE->second)); |
| 547 | } |
| 548 | return std::move(*Effect); |
| 549 | } |
| 550 | |
| 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. |
| 565 | if (SameFile) { |
| 566 | auto *Klass = Source->getDeclContext()->getOuterLexicalRecordContext(); |
| 567 | if (!Klass) |
| 568 | return error(Fmt: "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 | Range&: Tokens, Value: EndLoc, C: [](const syntax::Token &Tok, SourceLocation EndLoc) { |
| 574 | return Tok.location() < EndLoc; |
| 575 | }); |
| 576 | while (It != Tokens.end()) { |
| 577 | if (It->kind() != tok::semi) { |
| 578 | ++It; |
| 579 | continue; |
| 580 | } |
| 581 | unsigned Offset = Sel.AST->getSourceManager() |
| 582 | .getDecomposedLoc(Loc: It->endLocation()) |
| 583 | .second; |
| 584 | return InsertionPoint{Klass->getEnclosingNamespaceContext(), Offset}; |
| 585 | } |
| 586 | return error( |
| 587 | Fmt: "failed to determine insertion location: no end of class found" ); |
| 588 | } |
| 589 | |
| 590 | auto Region = getEligiblePoints( |
| 591 | Contents, Source->getQualifiedNameAsString(), Sel.AST->getLangOpts()); |
| 592 | |
| 593 | assert(!Region.EligiblePoints.empty()); |
| 594 | auto Offset = positionToOffset(Contents, Region.EligiblePoints.back()); |
| 595 | if (!Offset) |
| 596 | return Offset.takeError(); |
| 597 | |
| 598 | auto TargetContext = |
| 599 | findContextForNS(Region.EnclosingNamespace, Source->getDeclContext()); |
| 600 | if (!TargetContext) |
| 601 | return error(Fmt: "define outline: couldn't find a context for target" ); |
| 602 | |
| 603 | return InsertionPoint{*TargetContext, *Offset}; |
| 604 | } |
| 605 | |
| 606 | private: |
| 607 | const FunctionDecl *Source = nullptr; |
| 608 | bool SameFile = false; |
| 609 | }; |
| 610 | |
| 611 | REGISTER_TWEAK(DefineOutline) |
| 612 | |
| 613 | } // namespace |
| 614 | } // namespace clangd |
| 615 | } // namespace clang |
| 616 | |