[clangd] Implementation of auto type expansion.

Add a tweak for clangd to replace an auto keyword to the deduced type.

This way a user can declare something with auto and then have the
IDE/clangd replace auto with whatever type clangd thinks it is. In case
of long/complext types this makes is reduces writing effort for the
user.

The functionality is similar to the hover over the auto keyword.

Example (from the header):

```
/// Before:
///    auto x = Something();
///    ^^^^
/// After:
///    MyClass x = Something();
///    ^^^^^^^
```

Patch by kuhnel! (Christian Kühnel)
Differential Revision: https://reviews.llvm.org/D62855

llvm-svn: 365792
This commit is contained in:
Sam McCall 2019-07-11 16:04:18 +00:00
parent 5cc7c9ab93
commit 9470142ca5
13 changed files with 489 additions and 4 deletions

View File

@ -169,5 +169,35 @@ llvm::Optional<SymbolID> getSymbolID(const IdentifierInfo &II,
return SymbolID(USR);
}
std::string shortenNamespace(const llvm::StringRef OriginalName,
const llvm::StringRef CurrentNamespace) {
llvm::SmallVector<llvm::StringRef, 8> OriginalParts;
llvm::SmallVector<llvm::StringRef, 8> CurrentParts;
llvm::SmallVector<llvm::StringRef, 8> 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

View File

@ -67,6 +67,22 @@ llvm::Optional<SymbolID> getSymbolID(const IdentifierInfo &II,
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::OtherClass>", "ns1")
/// --> "MyClass<ns1::OtherClass>"
std::string shortenNamespace(const llvm::StringRef OriginalName,
const llvm::StringRef CurrentNamespace);
} // namespace clangd
} // namespace clang

View File

@ -364,5 +364,18 @@ const Node *SelectionTree::commonAncestor() const {
return Ancestor;
}
const DeclContext& SelectionTree::Node::getDeclContext() const {
for (const Node* CurrentNode = this; CurrentNode != nullptr;
CurrentNode = CurrentNode->Parent) {
if (const Decl* Current = CurrentNode->ASTNode.get<Decl>()) {
if (CurrentNode != this)
if (auto *DC = dyn_cast<DeclContext>(Current))
return *DC;
return *Current->getDeclContext();
}
}
llvm_unreachable("A tree must always be rooted at TranslationUnitDecl.");
}
} // namespace clangd
} // namespace clang

View File

@ -93,6 +93,9 @@ public:
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,
// which is not the node itself.
const DeclContext& getDeclContext() const;
};
// The most specific common ancestor of all the selected nodes.

View File

@ -870,7 +870,9 @@ public:
} // 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<QualType> 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.
@ -878,12 +880,20 @@ bool hasDeducedType(ParsedAST &AST, SourceLocation SourceLocationBeg) {
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<HoverInfo> getHover(ParsedAST &AST, Position Pos,

View File

@ -141,6 +141,16 @@ llvm::Optional<TypeHierarchyItem> getTypeHierarchy(
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<QualType> 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

View File

@ -18,6 +18,7 @@ add_clang_library(clangDaemonTweaks OBJECT
RawStringLiteral.cpp
SwapIfBranches.cpp
ExtractVariable.cpp
ExpandAutoType.cpp
LINK_LIBS
clangAST

View File

@ -0,0 +1,119 @@
//===--- 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 <climits>
#include <memory>
#include <string>
#include <AST.h>
#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<Effect> 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<clang::AutoTypeLoc> 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<TypeLoc>()) {
if (const AutoTypeLoc Result = TypeNode->getAs<AutoTypeLoc>()) {
CachedLocation = Result;
}
}
}
return (bool) CachedLocation;
}
Expected<Tweak::Effect> ExpandAutoType::apply(const Selection& Inputs) {
auto& SrcMgr = Inputs.AST.getASTContext().getSourceManager();
llvm::Optional<clang::QualType> 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<RecordType>(*DeducedType) and
dyn_cast<RecordType>(*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.
// FIXME: there are other types that have similar problems
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

View File

@ -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"}
---

View File

@ -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<testns1::OtherClass>",
shortenNamespace(
"testns1::TestClass<testns1::OtherClass>", "testns1"));
}
} // namespace
} // namespace clangd
} // namespace clang

View File

@ -23,6 +23,7 @@ endif()
add_custom_target(ClangdUnitTests)
add_unittest(ClangdUnitTests ClangdTests
Annotations.cpp
ASTTests.cpp
BackgroundIndexTests.cpp
CancellationTests.cpp
CanonicalIncludesTests.cpp

View File

@ -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 @@ void checkTransform(llvm::StringRef ID, llvm::StringRef Input,
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);
ASSERT_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";
@ -517,6 +531,140 @@ int a = 123 EMPTY_FN(1) ;
)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 <typename T> 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);
// replace array types
Input = R"cpp(
au^to x = "test";
)cpp";
Output = R"cpp(
const char * x = "test";
)cpp";
checkTransform(ID, Input, Output);
}
} // namespace
} // namespace clangd
} // namespace clang

View File

@ -2139,6 +2139,28 @@ TEST(FindReferences, NoQueryForLocalSymbols) {
}
}
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