diff --git a/clang-tools-extra/clangd/AST.h b/clang-tools-extra/clangd/AST.h --- a/clang-tools-extra/clangd/AST.h +++ b/clang-tools-extra/clangd/AST.h @@ -67,6 +67,22 @@ const MacroInfo *MI, const SourceManager &SM); +/// Returns a QualType as string. +std::string printType(const QualType QT, const DeclContext & Context); + +/// Try to shorten the OriginalName by removing namespaces from the left of +/// the string that are redundant in the CurrentNamespace. This way the type +/// idenfier become shorter and easier to read. +/// Limitation: It only handles the qualifier of the type itself, not that of +/// templates. +/// FIXME: change type of parameter CurrentNamespace to DeclContext , +/// take in to account using directives etc +/// Example: shortenNamespace("ns1::MyClass", "ns1") +/// --> "MyClass" +std::string shortenNamespace(const llvm::StringRef OriginalName, + const llvm::StringRef CurrentNamespace); + + } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/AST.cpp b/clang-tools-extra/clangd/AST.cpp --- a/clang-tools-extra/clangd/AST.cpp +++ b/clang-tools-extra/clangd/AST.cpp @@ -169,5 +169,35 @@ return SymbolID(USR); } +std::string shortenNamespace(const llvm::StringRef OriginalName, + const llvm::StringRef CurrentNamespace) { + llvm::SmallVector OriginalParts; + llvm::SmallVector CurrentParts; + llvm::SmallVector Result; + OriginalName.split(OriginalParts, "::"); + CurrentNamespace.split(CurrentParts, "::"); + auto MinLength = std::min(CurrentParts.size(), OriginalParts.size()); + + unsigned DifferentAt = 0; + while (DifferentAt < MinLength && + CurrentParts[DifferentAt] == OriginalParts[DifferentAt]) { + DifferentAt++; + } + + for (u_int i = DifferentAt; i < OriginalParts.size(); ++i) { + Result.push_back(OriginalParts[i]); + } + return join(Result, "::"); +} + +std::string printType(const QualType QT, const DeclContext & Context){ + PrintingPolicy PP(Context.getParentASTContext().getPrintingPolicy()); + PP.SuppressTagKeyword = 1; + return shortenNamespace( + QT.getAsString(PP), + printNamespaceScope(Context) ); +} + + } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/Selection.h b/clang-tools-extra/clangd/Selection.h --- a/clang-tools-extra/clangd/Selection.h +++ b/clang-tools-extra/clangd/Selection.h @@ -93,6 +93,9 @@ ast_type_traits::DynTypedNode ASTNode; // The extent to which this node is covered by the selection. Selection Selected; + // Walk up the AST to get the DeclContext of this Node. + const DeclContext& getDeclContext() const; + }; // The most specific common ancestor of all the selected nodes. diff --git a/clang-tools-extra/clangd/Selection.cpp b/clang-tools-extra/clangd/Selection.cpp --- a/clang-tools-extra/clangd/Selection.cpp +++ b/clang-tools-extra/clangd/Selection.cpp @@ -366,5 +366,18 @@ } } +const DeclContext& SelectionTree::Node::getDeclContext() const { + for (const Node* CurrentNode = this; CurrentNode != nullptr; + CurrentNode = CurrentNode->Parent) { + if (const Decl* Current = CurrentNode->ASTNode.get()) { + return *Current->getDeclContext(); + } + } + // A tree must always be rooted at TranslationUnitDecl, so this part + // should not be reachable. + assert(!"Unreachable code"); + // FIXME: What should we return here? +} + } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/XRefs.h b/clang-tools-extra/clangd/XRefs.h --- a/clang-tools-extra/clangd/XRefs.h +++ b/clang-tools-extra/clangd/XRefs.h @@ -140,6 +140,16 @@ ParsedAST &AST, Position Pos, int Resolve, TypeHierarchyDirection Direction, const SymbolIndex *Index = nullptr, PathRef TUPath = PathRef{}); +/// Retrieves the deduced type at a given location (auto, decltype). +/// Retuns None unless SourceLocationBeg starts an auto/decltype token. +/// It will return the underlying type. +llvm::Optional getDeducedType(ParsedAST &AST, + SourceLocation SourceLocationBeg); + +/// Check if there is a deduced type at a given location (auto, decltype). +/// SourceLocationBeg must point to the first character of the token +bool hasDeducedType(ParsedAST &AST, SourceLocation SourceLocationBeg); + } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp --- a/clang-tools-extra/clangd/XRefs.cpp +++ b/clang-tools-extra/clangd/XRefs.cpp @@ -842,7 +842,9 @@ } // namespace /// Retrieves the deduced type at a given location (auto, decltype). -bool hasDeducedType(ParsedAST &AST, SourceLocation SourceLocationBeg) { +/// SourceLocationBeg must point to the first character of the token +llvm::Optional getDeducedType(ParsedAST &AST, + SourceLocation SourceLocationBeg) { Token Tok; auto &ASTCtx = AST.getASTContext(); // Only try to find a deduced type if the token is auto or decltype. @@ -850,12 +852,20 @@ Lexer::getRawToken(SourceLocationBeg, Tok, ASTCtx.getSourceManager(), ASTCtx.getLangOpts(), false) || !Tok.is(tok::raw_identifier)) { - return false; + return {}; } AST.getPreprocessor().LookUpIdentifierInfo(Tok); if (!(Tok.is(tok::kw_auto) || Tok.is(tok::kw_decltype))) - return false; - return true; + return {}; + + DeducedTypeVisitor V(SourceLocationBeg); + V.TraverseAST(AST.getASTContext()); + return V.DeducedType; +} + +/// Retrieves the deduced type at a given location (auto, decltype). +bool hasDeducedType(ParsedAST &AST, SourceLocation SourceLocationBeg) { + return (bool) getDeducedType(AST, SourceLocationBeg); } llvm::Optional getHover(ParsedAST &AST, Position Pos, 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 @@ -18,6 +18,7 @@ RawStringLiteral.cpp SwapIfBranches.cpp ExtractVariable.cpp + ExpandAutoType.cpp LINK_LIBS clangAST diff --git a/clang-tools-extra/clangd/refactor/tweaks/ExpandAutoType.cpp b/clang-tools-extra/clangd/refactor/tweaks/ExpandAutoType.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/refactor/tweaks/ExpandAutoType.cpp @@ -0,0 +1,118 @@ +//===--- ReplaceAutoType.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 "refactor/Tweak.h" + +#include "Logger.h" +#include "clang/AST/Type.h" +#include "clang/AST/TypeLoc.h" +#include "clang/Basic/LLVM.h" +#include "llvm/ADT/None.h" +#include "llvm/ADT/Optional.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/Error.h" +#include +#include +#include +#include +#include "XRefs.h" +#include "llvm/ADT/StringExtras.h" + +namespace clang { +namespace clangd { + +/// Expand the "auto" type to the derived type +/// Before: +/// auto x = Something(); +/// ^^^^ +/// After: +/// MyClass x = Something(); +/// ^^^^^^^ +/// FIXME: Handle decltype as well +class ExpandAutoType : public Tweak { +public: + const char *id() const final; + Intent intent() const override { return Intent::Refactor;} + bool prepare(const Selection &Inputs) override; + Expected apply(const Selection &Inputs) override; + std::string title() const override; + +private: + /// Cache the AutoTypeLoc, so that we do not need to search twice. + llvm::Optional CachedLocation; + + /// Create an error message with filename and line number in it + llvm::Error createErrorMessage(const std::string& Message, + const Selection &Inputs); + +}; + +REGISTER_TWEAK(ExpandAutoType) + +std::string ExpandAutoType::title() const { return "expand auto type"; } + +bool ExpandAutoType::prepare(const Selection& Inputs) { + CachedLocation = llvm::None; + if (auto *Node = Inputs.ASTSelection.commonAncestor()) { + if (auto *TypeNode = Node->ASTNode.get()) { + if (const AutoTypeLoc Result = TypeNode->getAs()) { + CachedLocation = Result; + } + } + } + return (bool) CachedLocation; +} + +Expected ExpandAutoType::apply(const Selection& Inputs) { + auto& SrcMgr = Inputs.AST.getASTContext().getSourceManager(); + + llvm::Optional DeducedType = + getDeducedType(Inputs.AST, CachedLocation->getBeginLoc()); + + // if we can't resolve the type, return an error message + if (DeducedType == llvm::None || DeducedType->isNull()) { + return createErrorMessage("Could not deduce type for 'auto' type", Inputs); + } + + // if it's a lambda expression, return an error message + if (isa(*DeducedType) and + dyn_cast(*DeducedType)->getDecl()->isLambda()) { + return createErrorMessage("Could not expand type of lambda expression", + Inputs); + } + + // if it's a function expression, return an error message + // naively replacing 'auto' with the type will break declarations. + if (DeducedType->getTypePtr()->isFunctionPointerType()) { + return createErrorMessage("Could not expand type of function pointer", + Inputs); + } + + std::string PrettyTypeName = printType(*DeducedType, + Inputs.ASTSelection.commonAncestor()->getDeclContext()); + + tooling::Replacement + Expansion(SrcMgr, CharSourceRange(CachedLocation->getSourceRange(), true), + PrettyTypeName); + + return Tweak::Effect::applyEdit(tooling::Replacements(Expansion)); +} + +llvm::Error ExpandAutoType::createErrorMessage(const std::string& Message, + const Selection& Inputs) { + auto& SrcMgr = Inputs.AST.getASTContext().getSourceManager(); + std::string ErrorMessage = + Message + ": " + + SrcMgr.getFilename(Inputs.Cursor).str() + " Line " + + std::to_string(SrcMgr.getExpansionLineNumber(Inputs.Cursor)); + + return llvm::createStringError(llvm::inconvertibleErrorCode(), + ErrorMessage.c_str()); +} + +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/test/code-action-request.test b/clang-tools-extra/clangd/test/code-action-request.test new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/test/code-action-request.test @@ -0,0 +1,70 @@ +# RUN: clangd -log=verbose -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"auto i = 0;"}}} +--- +{ + "jsonrpc": "2.0", + "id": 1, + "method": "textDocument/codeAction", + "params": { + "textDocument": { + "uri": "test:///main.cpp" + }, + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 0, + "character": 4 + } + }, + "context": { + "diagnostics": [] + } + } +} +# CHECK: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "arguments": [ +# CHECK-NEXT: { +# CHECK-NEXT: "file": "file:///clangd-test/main.cpp", +# CHECK-NEXT: "selection": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 4, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "tweakID": "ExpandAutoType" +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "command": "clangd.applyTweak", +# CHECK-NEXT: "title": "expand auto type" +# CHECK-NEXT: } +# CHECK-NEXT: ] +--- +{"jsonrpc":"2.0","id":4,"method":"workspace/executeCommand","params":{"command":"clangd.applyTweak","arguments":[{"file":"file:///clangd-test/main.cpp","selection":{"end":{"character":4,"line":0},"start":{"character":0,"line":0}},"tweakID":"ExpandAutoType"}]}} +# CHECK: "newText": "int", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 4, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 0, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":4,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} +--- \ No newline at end of file diff --git a/clang-tools-extra/clangd/unittests/ASTTests.cpp b/clang-tools-extra/clangd/unittests/ASTTests.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/unittests/ASTTests.cpp @@ -0,0 +1,42 @@ +//===-- ASTTests.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 "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +TEST(ExpandAutoType, ShortenNamespace) { + ASSERT_EQ("TestClass", shortenNamespace("TestClass", "")); + + ASSERT_EQ("TestClass", shortenNamespace( + "testnamespace::TestClass", "testnamespace")); + + ASSERT_EQ( + "namespace1::TestClass", + shortenNamespace("namespace1::TestClass", "namespace2")); + + ASSERT_EQ("TestClass", + shortenNamespace("testns1::testns2::TestClass", + "testns1::testns2")); + + ASSERT_EQ( + "testns2::TestClass", + shortenNamespace("testns1::testns2::TestClass", "testns1")); + + ASSERT_EQ("TestClass", + shortenNamespace( + "testns1::TestClass", "testns1")); +} + + +} // namespace +} // namespace clangd +} // namespace clang diff --git a/clang-tools-extra/clangd/unittests/CMakeLists.txt b/clang-tools-extra/clangd/unittests/CMakeLists.txt --- a/clang-tools-extra/clangd/unittests/CMakeLists.txt +++ b/clang-tools-extra/clangd/unittests/CMakeLists.txt @@ -23,6 +23,7 @@ add_custom_target(ClangdUnitTests) add_unittest(ClangdUnitTests ClangdTests Annotations.cpp + ASTTests.cpp BackgroundIndexTests.cpp CancellationTests.cpp CanonicalIncludesTests.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 @@ -11,6 +11,7 @@ #include "TestTU.h" #include "refactor/Tweak.h" #include "clang/AST/Expr.h" +#include "clang/Basic/LLVM.h" #include "clang/Rewrite/Core/Rewriter.h" #include "clang/Tooling/Core/Replacement.h" #include "llvm/ADT/StringRef.h" @@ -126,6 +127,19 @@ EXPECT_EQ(Output, std::string(*Result)) << Input; } +/// Check if apply returns an error and that the @ErrorMessage is contained +/// in that error +void checkApplyContainsError(llvm::StringRef ID, llvm::StringRef Input, + const std::string& ErrorMessage) { + auto Result = apply(ID, Input); + EXPECT_FALSE(Result) << "expected error message:\n " << ErrorMessage << + "\non input:" << Input; + EXPECT_NE(std::string::npos, + llvm::toString(Result.takeError()).find(ErrorMessage)) + << "Wrong error message:\n " << llvm::toString(Result.takeError()) + << "\nexpected:\n " << ErrorMessage; +} + TEST(TweakTest, SwapIfBranches) { llvm::StringLiteral ID = "SwapIfBranches"; @@ -509,6 +523,132 @@ )cpp"); } +TEST(TweakTest, ExpandAutoType) { + llvm::StringLiteral ID = "ExpandAutoType"; + + checkAvailable(ID, R"cpp( + ^a^u^t^o^ i = 0; + )cpp"); + + checkNotAvailable(ID, R"cpp( + auto ^i^ ^=^ ^0^;^ + )cpp"); + + llvm::StringLiteral Input = R"cpp( + [[auto]] i = 0; + )cpp"; + llvm::StringLiteral Output = R"cpp( + int i = 0; + )cpp"; + checkTransform(ID, Input, Output); + + // check primitive type + Input = R"cpp( + au^to i = 0; + )cpp"; + Output = R"cpp( + int i = 0; + )cpp"; + checkTransform(ID, Input, Output); + + // check classes and namespaces + Input = R"cpp( + namespace testns { + class TestClass { + class SubClass {}; + }; + } + ^auto C = testns::TestClass::SubClass(); + )cpp"; + Output = R"cpp( + namespace testns { + class TestClass { + class SubClass {}; + }; + } + testns::TestClass::SubClass C = testns::TestClass::SubClass(); + )cpp"; + checkTransform(ID, Input, Output); + + // check that namespaces are shortened + Input = R"cpp( + namespace testns { + class TestClass { + }; + void func() { ^auto C = TestClass(); } + } + )cpp"; + Output = R"cpp( + namespace testns { + class TestClass { + }; + void func() { TestClass C = TestClass(); } + } + )cpp"; + checkTransform(ID, Input, Output); + + // unknown types in a template should not be replaced + Input = R"cpp( + template void x() { + ^auto y = T::z(); + } + )cpp"; + checkApplyContainsError(ID, Input, "Could not deduce type for 'auto' type"); + + // undefined functions should not be replaced + Input = R"cpp( + a^uto x = doesnt_exist(); + )cpp"; + checkApplyContainsError(ID, Input, "Could not deduce type for 'auto' type"); + + // function pointers should not be replaced + Input = R"cpp( + int foo(); + au^to x = &foo; + )cpp"; + checkApplyContainsError(ID, Input, + "Could not expand type of function pointer"); + + // lambda types are not replaced + Input = R"cpp( + au^to x = []{}; + )cpp"; + checkApplyContainsError(ID, Input, + "Could not expand type of lambda expression"); + + // inline namespaces + Input = R"cpp( + inline namespace x { + namespace { struct S; } + } + au^to y = S(); + )cpp"; + Output = R"cpp( + inline namespace x { + namespace { struct S; } + } + S y = S(); + )cpp"; + + // local class + Input = R"cpp( + namespace x { + void y() { + struct S{}; + a^uto z = S(); + }} + )cpp"; + Output = R"cpp( + namespace x { + void y() { + struct S{}; + S z = S(); + }} + )cpp"; + checkTransform(ID, Input, Output); + +} + } // namespace } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/unittests/XRefsTests.cpp b/clang-tools-extra/clangd/unittests/XRefsTests.cpp --- a/clang-tools-extra/clangd/unittests/XRefsTests.cpp +++ b/clang-tools-extra/clangd/unittests/XRefsTests.cpp @@ -2108,6 +2108,28 @@ } } +TEST(GetDeducedType, KwAutoExpansion) { + struct Test { + StringRef AnnotatedCode; + const char *DeducedType; + } Tests[] = { + {"^auto i = 0;", "int"}, + {"^auto f(){ return 1;};", "int"} + }; + for (Test T : Tests) { + Annotations File(T.AnnotatedCode); + auto AST = TestTU::withCode(File.code()).build(); + ASSERT_TRUE(AST.getDiagnostics().empty()) << AST.getDiagnostics().begin()->Message; + SourceManagerForFile SM("foo.cpp", File.code()); + + for (Position Pos : File.points()) { + auto Location = sourceLocationInMainFile(SM.get(), Pos); + auto DeducedType = getDeducedType(AST, *Location); + EXPECT_EQ(DeducedType->getAsString(), T.DeducedType); + } + } +} + } // namespace } // namespace clangd } // namespace clang