diff --git a/clang-tools-extra/clangd/refactor/tweaks/AddUsing.cpp b/clang-tools-extra/clangd/refactor/tweaks/AddUsing.cpp index 103e13f44d060..1e51d8fb9a518 100644 --- a/clang-tools-extra/clangd/refactor/tweaks/AddUsing.cpp +++ b/clang-tools-extra/clangd/refactor/tweaks/AddUsing.cpp @@ -8,10 +8,25 @@ #include "AST.h" #include "Config.h" +#include "SourceCode.h" #include "refactor/Tweak.h" #include "support/Logger.h" #include "clang/AST/Decl.h" +#include "clang/AST/Expr.h" +#include "clang/AST/NestedNameSpecifier.h" #include "clang/AST/RecursiveASTVisitor.h" +#include "clang/AST/Type.h" +#include "clang/AST/TypeLoc.h" +#include "clang/Basic/LLVM.h" +#include "clang/Basic/SourceLocation.h" +#include "clang/Tooling/Core/Replacement.h" +#include "clang/Tooling/Syntax/Tokens.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/raw_ostream.h" +#include +#include +#include namespace clang { namespace clangd { @@ -45,8 +60,12 @@ class AddUsing : public Tweak { // All of the following are set by prepare(). // The qualifier to remove. NestedNameSpecifierLoc QualifierToRemove; - // The name following QualifierToRemove. - llvm::StringRef Name; + // Qualified name to use when spelling the using declaration. This might be + // different than SpelledQualifier in presence of error correction. + std::string QualifierToSpell; + // The name and qualifier as spelled in the code. + llvm::StringRef SpelledQualifier; + llvm::StringRef SpelledName; // If valid, the insertion point for "using" statement must come after this. // This is relevant when the type is defined in the main file, to make sure // the type/function is already defined at the point where "using" is added. @@ -56,7 +75,7 @@ REGISTER_TWEAK(AddUsing) std::string AddUsing::title() const { return std::string(llvm::formatv( - "Add using-declaration for {0} and remove qualifier", Name)); + "Add using-declaration for {0} and remove qualifier", SpelledName)); } // Locates all "using" statements relevant to SelectionDeclContext. @@ -269,36 +288,23 @@ bool AddUsing::prepare(const Selection &Inputs) { if (Node == nullptr) return false; + SourceRange SpelledNameRange; if (auto *D = Node->ASTNode.get()) { if (auto *II = D->getDecl()->getIdentifier()) { QualifierToRemove = D->getQualifierLoc(); - Name = II->getName(); + SpelledNameRange = D->getSourceRange(); MustInsertAfterLoc = D->getDecl()->getBeginLoc(); } } else if (auto *T = Node->ASTNode.get()) { if (auto E = T->getAs()) { QualifierToRemove = E.getQualifierLoc(); - if (!QualifierToRemove) - return false; - auto NameRange = E.getSourceRange(); + SpelledNameRange = E.getSourceRange(); if (auto T = E.getNamedTypeLoc().getAs()) { // Remove the template arguments from the name. - NameRange.setEnd(T.getLAngleLoc().getLocWithOffset(-1)); + SpelledNameRange.setEnd(T.getLAngleLoc().getLocWithOffset(-1)); } - auto SpelledTokens = TB.spelledForExpanded(TB.expandedTokens(NameRange)); - if (!SpelledTokens) - return false; - auto SpelledRange = syntax::Token::range(SM, SpelledTokens->front(), - SpelledTokens->back()); - Name = SpelledRange.text(SM); - - std::string QualifierToRemoveStr = getNNSLAsString( - QualifierToRemove, Inputs.AST->getASTContext().getPrintingPolicy()); - if (!Name.consume_front(QualifierToRemoveStr)) - return false; // What's spelled doesn't match the qualifier. - if (const auto *ET = E.getTypePtr()) { if (const auto *TDT = dyn_cast(ET->getNamedType().getTypePtr())) { @@ -309,19 +315,14 @@ bool AddUsing::prepare(const Selection &Inputs) { } } } - - // 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 (!QualifierToRemove.hasQualifier() || + if (!QualifierToRemove || + // 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. !QualifierToRemove.getNestedNameSpecifier()->getAsNamespace() || - Name.empty()) { - return false; - } - - if (isNamespaceForbidden(Inputs, *QualifierToRemove.getNestedNameSpecifier())) + // Respect user config. + isNamespaceForbidden(Inputs, *QualifierToRemove.getNestedNameSpecifier())) return false; - // Macros are difficult. We only want to offer code action when what's spelled // under the cursor is a namespace qualifier. If it's a macro that expands to // a qualifier, user would not know what code action will actually change. @@ -333,23 +334,35 @@ bool AddUsing::prepare(const Selection &Inputs) { return false; } + auto SpelledTokens = + TB.spelledForExpanded(TB.expandedTokens(SpelledNameRange)); + if (!SpelledTokens) + return false; + auto SpelledRange = + syntax::Token::range(SM, SpelledTokens->front(), SpelledTokens->back()); + // We only drop qualifiers that're namespaces, so this is safe. + std::tie(SpelledQualifier, SpelledName) = + splitQualifiedName(SpelledRange.text(SM)); + QualifierToSpell = getNNSLAsString( + QualifierToRemove, Inputs.AST->getASTContext().getPrintingPolicy()); + if (!llvm::StringRef(QualifierToSpell).endswith(SpelledQualifier) || + SpelledName.empty()) + return false; // What's spelled doesn't match the qualifier. return true; } Expected AddUsing::apply(const Selection &Inputs) { auto &SM = Inputs.AST->getSourceManager(); - std::string QualifierToRemoveStr = getNNSLAsString( - QualifierToRemove, Inputs.AST->getASTContext().getPrintingPolicy()); tooling::Replacements R; if (auto Err = R.add(tooling::Replacement( SM, SM.getSpellingLoc(QualifierToRemove.getBeginLoc()), - QualifierToRemoveStr.length(), ""))) { + SpelledQualifier.size(), ""))) { return std::move(Err); } - auto InsertionPoint = - findInsertionPoint(Inputs, QualifierToRemove, Name, MustInsertAfterLoc); + auto InsertionPoint = findInsertionPoint(Inputs, QualifierToRemove, + SpelledName, MustInsertAfterLoc); if (!InsertionPoint) { return InsertionPoint.takeError(); } @@ -362,7 +375,7 @@ Expected AddUsing::apply(const Selection &Inputs) { if (InsertionPoint->AlwaysFullyQualify && !isFullyQualified(QualifierToRemove.getNestedNameSpecifier())) UsingTextStream << "::"; - UsingTextStream << QualifierToRemoveStr << Name << ";" + UsingTextStream << QualifierToSpell << SpelledName << ";" << InsertionPoint->Suffix; assert(SM.getFileID(InsertionPoint->Loc) == SM.getMainFileID()); diff --git a/clang-tools-extra/clangd/unittests/tweaks/AddUsingTests.cpp b/clang-tools-extra/clangd/unittests/tweaks/AddUsingTests.cpp index adfd018f56d27..d466dd5349d44 100644 --- a/clang-tools-extra/clangd/unittests/tweaks/AddUsingTests.cpp +++ b/clang-tools-extra/clangd/unittests/tweaks/AddUsingTests.cpp @@ -8,8 +8,11 @@ #include "Config.h" #include "TweakTesting.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringRef.h" #include "gmock/gmock.h" #include "gtest/gtest.h" +#include namespace clang { namespace clangd { @@ -30,7 +33,7 @@ namespace one { void oo() {} template class tt {}; namespace two { -enum ee {}; +enum ee { ee_enum_value }; void ff() {} class cc { public: @@ -64,9 +67,6 @@ class cc { EXPECT_UNAVAILABLE(Header + "void fun() { ::ban::fo^o(); }"); EXPECT_AVAILABLE(Header + "void fun() { banana::fo^o(); }"); - // Do not offer code action on typo-corrections. - EXPECT_UNAVAILABLE(Header + "/*error-ok*/c^c C;"); - // NestedNameSpecifier, but no namespace. EXPECT_UNAVAILABLE(Header + "class Foo {}; class F^oo foo;"); @@ -466,7 +466,37 @@ one::v^ec foo; using one::vec; vec foo; -)cpp"}}; +)cpp"}, + // Typo correction. + {R"cpp( +// error-ok +#include "test.hpp" +c^c C; +)cpp", + R"cpp( +// error-ok +#include "test.hpp" +using one::two::cc; + +cc C; +)cpp"}, + {R"cpp( +// error-ok +#include "test.hpp" +void foo() { + switch(one::two::ee{}) { case two::ee_^one:break; } +} +)cpp", + R"cpp( +// error-ok +#include "test.hpp" +using one::two::ee_one; + +void foo() { + switch(one::two::ee{}) { case ee_one:break; } +} +)cpp"}, + }; llvm::StringMap EditedFiles; for (const auto &Case : Cases) { ExtraFiles["test.hpp"] = R"cpp( @@ -484,6 +514,8 @@ class cc { using uu = two::cc; template struct vec {}; })cpp"; + // Typo correction is disabled in msvc-compatibility mode. + ExtraArgs.push_back("-fno-ms-compatibility"); EXPECT_EQ(apply(Case.TestSource, &EditedFiles), Case.ExpectedSource); } }