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 @@ -17,6 +17,7 @@ #include "clang/AST/Decl.h" #include "clang/Basic/SourceLocation.h" #include "clang/Lex/MacroInfo.h" +#include "clang/AST/RecursiveASTVisitor.h" namespace clang { class SourceManager; @@ -67,6 +68,9 @@ const MacroInfo *MI, const SourceManager &SM); +// TODO: add documentation +llvm::Optional getDeductedType(SourceLocation SearchedLocation, ASTContext &AST); + } // 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,105 @@ return SymbolID(USR); } +namespace { +/// Computes the deduced type at a given location by visiting the relevant +/// nodes. We use this to display the actual type when hovering over an "auto" +/// keyword or "decltype()" expression. +/// FIXME: This could have been a lot simpler by visiting AutoTypeLocs but it +/// seems that the AutoTypeLocs that can be visited along with their AutoType do +/// not have the deduced type set. Instead, we have to go to the appropriate +/// DeclaratorDecl/FunctionDecl and work our back to the AutoType that does have +/// a deduced type set. The AST should be improved to simplify this scenario. +class DeducedTypeVisitor : public RecursiveASTVisitor { + SourceLocation SearchedLocation; + llvm::Optional DeducedType; + +public: + DeducedTypeVisitor(SourceLocation SearchedLocation) + : SearchedLocation(SearchedLocation) {} + + llvm::Optional getDeducedType() { return DeducedType; } + + // Handle auto initializers: + //- auto i = 1; + //- decltype(auto) i = 1; + //- auto& i = 1; + //- auto* i = &a; + bool VisitDeclaratorDecl(DeclaratorDecl *D) { + if (!D->getTypeSourceInfo() || + D->getTypeSourceInfo()->getTypeLoc().getBeginLoc() != SearchedLocation) + return true; + + if (auto *AT = D->getType()->getContainedAutoType()) { + if (!AT->getDeducedType().isNull()) + DeducedType = AT->getDeducedType(); + } + return true; + } + + // Handle auto return types: + //- auto foo() {} + //- auto& foo() {} + //- auto foo() -> int {} + //- auto foo() -> decltype(1+1) {} + //- operator auto() const { return 10; } + bool VisitFunctionDecl(FunctionDecl *D) { + if (!D->getTypeSourceInfo()) + return true; + // Loc of auto in return type (c++14). + auto CurLoc = D->getReturnTypeSourceRange().getBegin(); + // Loc of "auto" in operator auto() + if (CurLoc.isInvalid() && dyn_cast(D)) + CurLoc = D->getTypeSourceInfo()->getTypeLoc().getBeginLoc(); + // Loc of "auto" in function with traling return type (c++11). + if (CurLoc.isInvalid()) + CurLoc = D->getSourceRange().getBegin(); + if (CurLoc != SearchedLocation) + return true; + + const AutoType *AT = D->getReturnType()->getContainedAutoType(); + if (AT && !AT->getDeducedType().isNull()) { + DeducedType = AT->getDeducedType(); + } else if (auto DT = dyn_cast(D->getReturnType())) { + // auto in a trailing return type just points to a DecltypeType and + // getContainedAutoType does not unwrap it. + if (!DT->getUnderlyingType().isNull()) + DeducedType = DT->getUnderlyingType(); + } else if (!D->getReturnType().isNull()) { + DeducedType = D->getReturnType(); + } + return true; + } + + // Handle non-auto decltype, e.g.: + // - auto foo() -> decltype(expr) {} + // - decltype(expr); + bool VisitDecltypeTypeLoc(DecltypeTypeLoc TL) { + if (TL.getBeginLoc() != SearchedLocation) + return true; + + // A DecltypeType's underlying type can be another DecltypeType! E.g. + // int I = 0; + // decltype(I) J = I; + // decltype(J) K = J; + const DecltypeType *DT = dyn_cast(TL.getTypePtr()); + while (DT && !DT->getUnderlyingType().isNull()) { + DeducedType = DT->getUnderlyingType(); + DT = dyn_cast(DeducedType->getTypePtr()); + } + return true; + } +}; +} // namespace + +llvm::Optional getDeductedType(SourceLocation SearchedLocation, ASTContext &AST){ + auto Visitor = DeducedTypeVisitor(SearchedLocation); + Visitor.TraverseAST(AST); + return Visitor.getDeducedType(); +}; + + + + } // 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 @@ -138,6 +138,11 @@ getTypeHierarchy(ParsedAST &AST, Position Pos, int Resolve, TypeHierarchyDirection Direction); +/// Retrieves the deduced type at a given location (auto, decltype). +/// SourceLocationBeg must point to the first character of the token +llvm::Optional getDeducedType(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 @@ -847,6 +847,13 @@ /// Retrieves the deduced type at a given location (auto, decltype). bool hasDeducedType(ParsedAST &AST, SourceLocation SourceLocationBeg) { + return getDeducedType(AST, SourceLocationBeg) != llvm::None; +} + +/// Retrieves the deduced type at a given location (auto, decltype). +/// 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. @@ -854,12 +861,15 @@ 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; } 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 @@ -14,6 +14,7 @@ add_clang_library(clangDaemonTweaks OBJECT RawStringLiteral.cpp SwapIfBranches.cpp + ExpandAutoType.cpp LINK_LIBS clangAST diff --git a/clang-tools-extra/clangd/refactor/tweaks/ExpandAutoType.h b/clang-tools-extra/clangd/refactor/tweaks/ExpandAutoType.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/refactor/tweaks/ExpandAutoType.h @@ -0,0 +1,46 @@ +//===--- Tweak.h -------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_REFACTOR_ACTIONS_TWEAKS_EXPANDAUTO_TYPE_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_REFACTOR_ACTIONS_TWEAKS_EXPANDAUTO_TYPE_H +#include "refactor/Tweak.h" + +namespace clang { +namespace clangd { +/// Expand the "auto" type to the derived type +/// Before: +/// auto x = Something(); +/// ^^^^ +/// After: +/// std::string x = Something(); +/// ^^^^^^^^^^^ +class ExpandAutoType : public Tweak { +public: + const char *id() const override final; + bool prepare(const Selection &Inputs) override; + Expected apply(const Selection &Inputs) override; + std::string title() const override; + +protected: + const llvm::Optional + findAutoType(const SelectionTree::Node *Node); + + static std::string getNamespaceString(const SelectionTree::Node *Node); + + static std::string shortenNamespace(const llvm::StringRef &OriginalName, + const llvm::StringRef &CurrentNamespace); + +private: + // cache the AutoTypeLoc, so that we do not need to search twice + llvm::Optional CachedLocation; +}; + +} // namespace clangd +} // namespace clang + +#endif \ No newline at end of file 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,113 @@ +//===--- 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 "Logger.h" +#include "clang/AST/Type.h" +#include "clang/AST/TypeLoc.h" +#include "llvm/ADT/None.h" +#include "llvm/ADT/Optional.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/Error.h" +#include +#include +#include "XRefs.h" +#include "ExpandAutoType.h" +#include "llvm/ADT/StringExtras.h" + +namespace clang { +namespace clangd { + +REGISTER_TWEAK(ExpandAutoType) + +bool ExpandAutoType::prepare(const Selection &Inputs) { + auto Node = Inputs.ASTSelection.commonAncestor(); + CachedLocation = findAutoType(Node); + return CachedLocation != llvm::None; +} + +Expected ExpandAutoType::apply(const Selection &Inputs) { + auto &SrcMgr = Inputs.AST.getASTContext().getSourceManager(); + + llvm::Optional DeductedType = + getDeducedType(Inputs.AST, CachedLocation->getBeginLoc()); + if (DeductedType == llvm::None) { + log("could not deduct type for 'auto' type, not proposing any changes: %s Line %s", + SrcMgr.getFilename(Inputs.Cursor), + SrcMgr.getExpansionLineNumber(Inputs.Cursor)); + return tooling::Replacements(); + } + + SourceRange OriginalRange(CachedLocation->getBeginLoc(), + CachedLocation->getEndLoc()); + PrintingPolicy PP(Inputs.AST.getASTContext().getPrintingPolicy()); + PP.SuppressTagKeyword = true; + std::string PrettyTypeName = shortenNamespace( + DeductedType->getAsString(PP), + getNamespaceString(Inputs.ASTSelection.commonAncestor())); + tooling::Replacement Expansion(SrcMgr, CharSourceRange(OriginalRange, true), + PrettyTypeName); + + return tooling::Replacements(Expansion); +} + +// try to find an 'auto' type location from the Selection +const llvm::Optional +ExpandAutoType::findAutoType(const SelectionTree::Node *StartNode) { + auto Node = StartNode; + while (Node != nullptr) { + const TypeLoc *TypeNode = Node->ASTNode.get(); + if (TypeNode) { + if (const AutoTypeLoc Result = TypeNode->getAs()) { + return Result; + } + } + Node = Node->Parent; + } + return llvm::None; +} + +std::string ExpandAutoType::title() const { return "expand auto type"; } + +std::string +ExpandAutoType::shortenNamespace(const llvm::StringRef &OriginalName, + const llvm::StringRef &CurrentNamespace) { + llvm::SmallVector OriginalParts; + llvm::SmallVector CurrentParts; + llvm::SmallVector Result; + OriginalName.split(OriginalParts, "::"); + CurrentNamespace.split(CurrentParts, "::"); + unsigned MinLength = std::min(CurrentParts.size(), OriginalParts.size()); + + u_int DifferentAt = 0; + while (CurrentParts[DifferentAt] == OriginalParts[DifferentAt] && + DifferentAt < MinLength) { + DifferentAt++; + } + + for (u_int i = DifferentAt; i < OriginalParts.size(); ++i) { + Result.push_back(OriginalParts[i]); + } + return join(Result, "::"); +} + +std::string ExpandAutoType::getNamespaceString(const SelectionTree::Node *StartNode) { + auto Node = StartNode; + while (Node != nullptr) { + LLVM_DEBUG(Node->ASTNode.print()); + if (const Decl *Current = Node->ASTNode.get()) { + if (const clang::NamespaceDecl *CurrentNameSpace = + dyn_cast(Current)) { + return CurrentNameSpace->getQualifiedNameAsString(); + } + } + Node = Node->Parent; + } + return ""; +} + +} // 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/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 @@ -19,6 +19,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" #include +#include "refactor/tweaks/ExpandAutoType.h" using llvm::Failed; using llvm::HasValue; @@ -217,6 +218,111 @@ checkTransform(ID, Input, Output); } +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); +} + +TEST(ExpandAutoType, GetNamespaceString) { + struct EATWrapper : ExpandAutoType { + // to access the protected method + using ExpandAutoType::getNamespaceString; + }; + TestTU TU; + TU.Filename = "foo.cpp"; + Annotations Code("namespace firstns{namespace secondns{ au^to i = 0;} }"); + TU.Code = Code.code(); + ParsedAST AST = TU.build(); + ASSERT_TRUE(AST.getDiagnostics().empty()); + auto Tree = SelectionTree(AST.getASTContext(), + *positionToOffset(Code.code(), Code.point())); + ASSERT_EQ("firstns::secondns", EATWrapper::getNamespaceString(Tree.commonAncestor())); +} + +TEST(ExpandAutoType, ShortenNamespace) { + struct EATWrapper : ExpandAutoType { + // to access the protected method + using ExpandAutoType::shortenNamespace; + }; + + ASSERT_EQ("TestClass", + EATWrapper::shortenNamespace("TestClass", "")); + + ASSERT_EQ("TestClass", + EATWrapper::shortenNamespace("testnamespace::TestClass", "testnamespace")); + + ASSERT_EQ("namespace1::TestClass", + EATWrapper::shortenNamespace("namespace1::TestClass", "namespace2")); + + ASSERT_EQ("TestClass", + EATWrapper::shortenNamespace("testns1::testns2::TestClass", "testns1::testns2")); + + ASSERT_EQ( + "testns2::TestClass", + EATWrapper::shortenNamespace("testns1::testns2::TestClass", "testns1")); +} + } // 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 @@ -1971,6 +1971,28 @@ } } +TEST(GetDeductedType, KwAutoExpansion) { + struct Test { + StringRef AnnotatedCode; + const char *DeductedType; + } 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.DeductedType); + } + } +} + } // namespace } // namespace clangd } // namespace clang