diff --git a/clang-tools-extra/clangd/refactor/tweaks/AddUsing.cpp b/clang-tools-extra/clangd/refactor/tweaks/AddUsing.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/refactor/tweaks/AddUsing.cpp @@ -0,0 +1,238 @@ +//===--- AddUsing.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 "Logger.h" +#include "refactor/Tweak.h" +#include "clang/AST/Decl.h" +#include "clang/AST/RecursiveASTVisitor.h" + +namespace clang { +namespace clangd { +namespace { + +// Tweak for removing full namespace qualifier under cursor on function calls +// and types and adding "using" statement instead. +class AddUsing : public Tweak { +public: + const char *id() const override; + + bool prepare(const Selection &Inputs) override; + Expected apply(const Selection &Inputs) override; + std::string title() const override; + Intent intent() const override { return Refactor; } + +private: + struct InsertionPointData { + // True iff "using" already exists and we should not add it. + bool IdenticalUsingFound = false; + // Location to insert the "using" statement. + SourceLocation Loc; + // Extra suffix to place after the "using" statement. Depending on what the + // insertion point is anchored to, we may need one or more \n to ensure + // proper formatting. + std::string Suffix; + }; + // Finds the best place to insert the "using" statement. Returns invalid + // SourceLocation if the "using" statement already exists. + llvm::Expected + findInsertionPoint(const Selection &Inputs); + + // The qualifier to remove. Set by prepare(). + NestedNameSpecifierLoc NNSL; + // The name following NNSL. Set by prepare(). + llvm::StringRef Name; +}; +REGISTER_TWEAK(AddUsing) + +std::string AddUsing::title() const { + return std::string( + llvm::formatv("Add using statement and remove full qualifier.")); +} + +// Locates all "using" statements relevant to SelectionDeclContext. +class UsingFinder : public RecursiveASTVisitor { +public: + UsingFinder(std::vector &Results, + const DeclContext *SelectionDeclContext, const SourceManager &SM) + : Results(Results), SelectionDeclContext(SelectionDeclContext), SM(SM) {} + + bool VisitUsingDecl(UsingDecl *D) { + auto Loc = D->getUsingLoc(); + if (Loc.isInvalid() || Loc.isMacroID() || + SM.getFileID(Loc) != SM.getMainFileID()) { + return true; + } + if (D->getDeclContext()->Encloses(SelectionDeclContext)) { + Results.push_back(D); + } + return true; + } + +private: + std::vector &Results; + const DeclContext *SelectionDeclContext; + const SourceManager &SM; +}; + +bool AddUsing::prepare(const Selection &Inputs) { + auto *Node = Inputs.ASTSelection.commonAncestor(); + if (Node == nullptr) + return false; + + // If we're looking at a type or NestedNameSpecifier, walk up the tree until + // we find the "main" node we care about, which would be ElaboratedTypeLoc or + // DeclRefExpr. + while (Node->Parent) { + if (Node->ASTNode.get()) { + Node = Node->Parent; + } else if (Node->ASTNode.get() && + (Node->Parent->ASTNode.get() || + Node->Parent->ASTNode.get())) { + // Node is TypeLoc, but it's parent is either TypeLoc or + // NestedNameSpecifier. In both cases, we want to go up, to find + // the outermost TypeLoc. + Node = Node->Parent; + } else { + break; + } + } + if (Node == nullptr) + return false; + + if (auto *D = Node->ASTNode.get()) { + NNSL = D->getQualifierLoc(); + Name = D->getDecl()->getName(); + } else if (auto *T = Node->ASTNode.get()) { + if (auto E = T->getAs()) { + NNSL = E.getQualifierLoc(); + Name = + E.getType().getUnqualifiedType().getBaseTypeIdentifier()->getName(); + } + } + + // FIXME: This only supports removing qualifiers that are made up of just + // namespace names. If qualifier contains a type, we could take the longest + // namespace prefix and remove that. + if (!NNSL.hasQualifier() || + !NNSL.getNestedNameSpecifier()->getAsNamespace()) { + return false; + } + + return true; +} + +llvm::Expected +AddUsing::findInsertionPoint(const Selection &Inputs) { + auto &SM = Inputs.AST->getSourceManager(); + + // Search for all using decls that affect this point in file. We need this for + // two reasons: to skip adding "using" if one already exists and to find best + // place to add it, if it doesn't exist. + SourceLocation LastUsingLoc; + std::vector Usings; + UsingFinder(Usings, &Inputs.ASTSelection.commonAncestor()->getDeclContext(), + SM) + .TraverseAST(Inputs.AST->getASTContext()); + + llvm::sort(Usings, [&SM](const UsingDecl *L, const UsingDecl *R) { + return SM.isBeforeInTranslationUnit(L->getUsingLoc(), R->getUsingLoc()); + }); + for (auto &U : Usings) { + if (SM.isBeforeInTranslationUnit(Inputs.Cursor, U->getUsingLoc())) + break; + if (U->getQualifier()->getAsNamespace() == + NNSL.getNestedNameSpecifier()->getAsNamespace() && + U->getName() == Name) { + InsertionPointData Out; + Out.IdenticalUsingFound = true; + return Out; + } + // Insertion point will be before last UsingDecl that affects cursor + // position. For most cases this should stick with the local convention of + // add using inside or outside namespace. + // We could be smarter and check if the deepest block of "using" is sorted + // alphabetically and if so, insert at appropriate place. For now, users can + // just reformat the file with clang-format or similar. + LastUsingLoc = U->getUsingLoc(); + } + if (LastUsingLoc.isValid()) { + InsertionPointData Out; + Out.Loc = LastUsingLoc; + return Out; + } + + // No relevant "using" statements. Try the nearest namespace level. + const auto *NS = Inputs.ASTSelection.commonAncestor() + ->getDeclContext() + .getEnclosingNamespaceContext(); + if (auto *ND = dyn_cast(NS)) { + auto Toks = Inputs.AST->getTokens().expandedTokens(ND->getSourceRange()); + const auto *Tok = llvm::find_if(Toks, [](const syntax::Token &Tok) { + return Tok.kind() == tok::l_brace; + }); + if (Tok == Toks.end() || Tok->endLocation().isInvalid()) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "Namespace with no {"); + } + InsertionPointData Out; + Out.Loc = Tok->endLocation(); + Out.Suffix = "\n"; + return Out; + } + // No using, no namespace, no idea where to insert. Try above the first + // top level decl. + auto TLDs = Inputs.AST->getLocalTopLevelDecls(); + if (TLDs.empty()) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "Cannot find place to insert \"using\""); + } + InsertionPointData Out; + Out.Loc = TLDs[0]->getBeginLoc(); + Out.Suffix = "\n\n"; + return Out; +} + +Expected AddUsing::apply(const Selection &Inputs) { + auto &SM = Inputs.AST->getSourceManager(); + // Remove the qualifier from under cursor. + tooling::Replacements R; + if (auto Err = R.add(tooling::Replacement( + SM, CharSourceRange::getTokenRange(NNSL.getSourceRange()), "", + Inputs.AST->getLangOpts()))) { + return std::move(Err); + } + + auto InsertionPoint = findInsertionPoint(Inputs); + if (!InsertionPoint) { + return InsertionPoint.takeError(); + } + + if (!InsertionPoint->IdenticalUsingFound) { + // Add the using statement at appropriate location. + std::string UsingText; + llvm::raw_string_ostream UsingTextStream(UsingText); + UsingTextStream << "using "; + NNSL.getNestedNameSpecifier()->print( + UsingTextStream, Inputs.AST->getASTContext().getPrintingPolicy()); + UsingTextStream << Name << ";" << InsertionPoint->Suffix; + + if (auto Err = R.add(tooling::Replacement(SM, InsertionPoint->Loc, 0, + UsingTextStream.str()))) { + return std::move(Err); + } + } + + return Effect::mainFileEdit(Inputs.AST->getASTContext().getSourceManager(), + std::move(R)); +} + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt b/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt --- a/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt +++ b/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt @@ -12,6 +12,7 @@ # $ to a list of sources, see # clangd/tool/CMakeLists.txt for an example. add_clang_library(clangDaemonTweaks OBJECT + AddUsing.cpp AnnotateHighlightings.cpp DumpAST.cpp DefineInline.cpp diff --git a/clang-tools-extra/clangd/unittests/TweakTests.cpp b/clang-tools-extra/clangd/unittests/TweakTests.cpp --- a/clang-tools-extra/clangd/unittests/TweakTests.cpp +++ b/clang-tools-extra/clangd/unittests/TweakTests.cpp @@ -2390,6 +2390,191 @@ EXPECT_EQ(apply(Case.first), Case.second); } } + +TWEAK_TEST(AddUsing); +TEST_F(AddUsingTest, Prepare) { + const std::string Header = R"cpp( +namespace one { +void oo() {} +namespace two { +enum ee {}; +void ff() {} +class cc { +public: + struct st {}; + static void mm() {} +}; +} +})cpp"; + + EXPECT_AVAILABLE(Header + "void fun() { o^n^e^:^:^t^w^o^:^:^f^f(); }"); + EXPECT_AVAILABLE(Header + "void fun() { o^n^e^::^o^o(); }"); + EXPECT_AVAILABLE(Header + "void fun() { o^n^e^:^:^t^w^o^:^:^e^e E; }"); + EXPECT_AVAILABLE(Header + "void fun() { o^n^e^:^:^t^w^o:^:^c^c C; }"); + EXPECT_UNAVAILABLE(Header + + "void fun() { o^n^e^:^:^t^w^o^:^:^c^c^:^:^m^m(); }"); + EXPECT_UNAVAILABLE(Header + + "void fun() { o^n^e^:^:^t^w^o^:^:^c^c^:^:^s^t inst; }"); +} + +TEST_F(AddUsingTest, Apply) { + FileName = "test.cpp"; + struct { + llvm::StringRef TestSource; + llvm::StringRef ExpectedSource; + } Cases[]{{ + // Function, no other using, namespace. + R"cpp( +#include "test.hpp" +namespace { +void fun() { + ^o^n^e^:^:^t^w^o^:^:^f^f(); +} +})cpp", + R"cpp( +#include "test.hpp" +namespace {using one::two::ff; + +void fun() { + ff(); +} +})cpp", + }, + // Type, no other using, namespace. + { + R"cpp( +#include "test.hpp" +namespace { +void fun() { + on^e::t^wo::c^c inst; +} +})cpp", + R"cpp( +#include "test.hpp" +namespace {using one::two::cc; + +void fun() { + cc inst; +} +})cpp", + }, + // Type, no other using, no namespace. + { + R"cpp( +#include "test.hpp" + +void fun() { + on^e::t^wo::e^e inst; +})cpp", + R"cpp( +#include "test.hpp" + +using one::two::ee; + +void fun() { + ee inst; +})cpp"}, + // Function, other usings. + { + R"cpp( +#include "test.hpp" + +using one::two::cc; +using one::two::ee; + +namespace { +void fun() { + one::two::f^f(); +} +})cpp", + R"cpp( +#include "test.hpp" + +using one::two::cc; +using one::two::ff;using one::two::ee; + +namespace { +void fun() { + ff(); +} +})cpp", + }, + // Function, other usings inside namespace. + { + R"cpp( +#include "test.hpp" + +using one::two::cc; + +namespace { + +using one::two::ff; + +void fun() { + o^ne::o^o(); +} +})cpp", + R"cpp( +#include "test.hpp" + +using one::two::cc; + +namespace { + +using one::oo;using one::two::ff; + +void fun() { + oo(); +} +})cpp"}, + // Using comes after cursor. + { + R"cpp( +#include "test.hpp" + +namespace { + +void fun() { + one::t^wo::ff(); +} + +using one::two::cc; + +})cpp", + R"cpp( +#include "test.hpp" + +namespace {using one::two::ff; + + +void fun() { + ff(); +} + +using one::two::cc; + +})cpp"}}; + llvm::StringMap EditedFiles; + for (const auto &Case : Cases) { + for (const auto &SubCase : expandCases(Case.TestSource)) { + ExtraFiles["test.hpp"] = R"cpp( +namespace one { +void oo() {} +namespace two { +enum ee {}; +void ff() {} +class cc { +public: + struct st {}; + static void mm() {} +}; +} +})cpp"; + EXPECT_EQ(apply(SubCase, &EditedFiles), Case.ExpectedSource); + } + } +} + } // namespace } // namespace clangd } // namespace clang