//===--- 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 "Logger.h" #include "ParsedAST.h" #include "Path.h" #include "Selection.h" #include "SourceCode.h" #include "refactor/Tweak.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/Driver/Types.h" #include "clang/Format/Format.h" #include "clang/Lex/Lexer.h" #include "clang/Tooling/Core/Replacement.h" #include "clang/Tooling/Syntax/Tokens.h" #include "llvm/ADT/None.h" #include "llvm/ADT/Optional.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Casting.h" #include "llvm/Support/Error.h" #include #include 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 ast_type_traits::DynTypedNode &AstNode = SelNode->ASTNode; if (const FunctionDecl *FD = AstNode.get()) return FD; if (AstNode.get() && SelNode->Selected == SelectionTree::Complete) { if (const SelectionTree::Node *P = SelNode->Parent) return P->ASTNode.get(); } return nullptr; } llvm::Optional getSourceFile(llvm::StringRef FileName, const Tweak::Selection &Sel) { if (auto Source = getCorrespondingHeaderOrSource( FileName, &Sel.AST->getSourceManager().getFileManager().getVirtualFileSystem())) 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 None if TargetNS is not a prefix of CurContext. llvm::Optional findContextForNS(llvm::StringRef TargetNS, const DeclContext *CurContext) { assert(TargetNS.empty() || TargetNS.endswith("::")); // 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(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.startswith(TargetNS)) return llvm::None; 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 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 llvm::createStringError(llvm::inconvertibleErrorCode(), "Couldn't get range for function."); // Include template parameter list. if (auto *FTD = FD->getDescribedFunctionTemplate()) OrigFuncRange->setBegin(FTD->getBeginLoc()); // 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); } // 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 getFunctionSourceCode(const FunctionDecl *FD, llvm::StringRef TargetNamespace, const syntax::TokenBuffer &TokBuf) { auto &AST = FD->getASTContext(); auto &SM = AST.getSourceManager(); auto TargetContext = findContextForNS(TargetNamespace, FD->getDeclContext()); if (!TargetContext) return llvm::createStringError( llvm::inconvertibleErrorCode(), "define outline: couldn't find a context for target"); 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)); }); // 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()) { // Deletion range initially spans the initializer, 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()); }); // Find the last `=` before the default arg. 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), llvm::createStringError( llvm::inconvertibleErrorCode(), llvm::StringRef("define outline: Can't move out of line as " "function has a macro `") + A->getSpelling() + "` specifier.")); 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()); DelAttr(FD->getAttr()); if (FD->isVirtualAsWritten()) { SourceRange SpecRange{FD->getBeginLoc(), FD->getLocation()}; bool HasErrors = true; // Clang allows duplicating virtual specifiers so check for multiple // occurances. for (const auto &Tok : TokBuf.expandedTokens(SpecRange)) { if (Tok.kind() != tok::kw_virtual) continue; auto Spelling = TokBuf.spelledForExpanded(llvm::makeArrayRef(Tok)); if (!Spelling) { HasErrors = true; break; } HasErrors = false; CharSourceRange DelRange = syntax::Token::range(SM, Spelling->front(), Spelling->back()) .toCharRange(SM); if (auto Err = DeclarationCleanups.add(tooling::Replacement(SM, DelRange, ""))) Errors = llvm::joinErrors(std::move(Errors), std::move(Err)); } if (HasErrors) { Errors = llvm::joinErrors( std::move(Errors), llvm::createStringError(llvm::inconvertibleErrorCode(), "define outline: Can't move out of line as " "function has a macro `virtual` specifier.")); } } if (Errors) return std::move(Errors); return getFunctionSourceAfterReplacements(FD, DeclarationCleanups); } struct InsertionPoint { std::string EnclosingNamespace; size_t Offset; }; // 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. llvm::Expected getInsertionPoint(llvm::StringRef Contents, llvm::StringRef QualifiedName, const format::FormatStyle &Style) { auto Region = getEligiblePoints(Contents, QualifiedName, Style); assert(!Region.EligiblePoints.empty()); // FIXME: This selection can be made smarter by looking at the definition // locations for adjacent decls to Source. Unfortunately psudeo parsing in // getEligibleRegions only knows about namespace begin/end events so we // can't match function start/end positions yet. auto Offset = positionToOffset(Contents, Region.EligiblePoints.back()); if (!Offset) return Offset.takeError(); return InsertionPoint{Region.EnclosingNamespace, *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(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 true; } Intent intent() const override { return Intent::Refactor; } std::string title() const override { return "Move function body to out-of-line."; } bool prepare(const Selection &Sel) override { // Bail out if we are not in a header file. // FIXME: We might want to consider moving method definitions below class // definition even if we are inside a source file. if (!isHeaderFile(Sel.AST->getSourceManager().getFilename(Sel.Cursor), Sel.AST->getLangOpts())) return false; 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 in templated classes, as it is hard to spell the class name, i.e // if the template parameter is unnamed. if (auto *MD = llvm::dyn_cast(Source)) { if (MD->getParent()->isTemplated()) 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 apply(const Selection &Sel) override { const SourceManager &SM = Sel.AST->getSourceManager(); auto MainFileName = getCanonicalPath(SM.getFileEntryForID(SM.getMainFileID()), SM); if (!MainFileName) return llvm::createStringError( llvm::inconvertibleErrorCode(), "Couldn't get absolute path for mainfile."); auto CCFile = getSourceFile(*MainFileName, Sel); if (!CCFile) return llvm::createStringError( llvm::inconvertibleErrorCode(), "Couldn't find a suitable implementation file."); auto &FS = Sel.AST->getSourceManager().getFileManager().getVirtualFileSystem(); auto Buffer = FS.getBufferForFile(*CCFile); // FIXME: Maybe we should consider creating the implementation file if it // doesn't exist? if (!Buffer) return llvm::createStringError(Buffer.getError(), Buffer.getError().message()); auto Contents = Buffer->get()->getBuffer(); auto InsertionPoint = getInsertionPoint(Contents, Source->getQualifiedNameAsString(), getFormatStyleForFile(*CCFile, Contents, &FS)); if (!InsertionPoint) return InsertionPoint.takeError(); auto FuncDef = getFunctionSourceCode( Source, InsertionPoint->EnclosingNamespace, Sel.AST->getTokens()); 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(); // FIXME: We should also get rid of inline qualifier. const tooling::Replacement DeleteFuncBody( Sel.AST->getSourceManager(), CharSourceRange::getTokenRange(*toHalfOpenFileRange( SM, Sel.AST->getLangOpts(), getDeletionRange(Source, Sel.AST->getTokens()))), ";"); auto HeaderFE = Effect::fileEdit(SM, SM.getMainFileID(), tooling::Replacements(DeleteFuncBody)); if (!HeaderFE) return HeaderFE.takeError(); Effect->ApplyEdits.try_emplace(HeaderFE->first, std::move(HeaderFE->second)); return std::move(*Effect); } private: const FunctionDecl *Source = nullptr; }; REGISTER_TWEAK(DefineOutline) } // namespace } // namespace clangd } // namespace clang