[clang-tools-extra] 8adc4d1 - [clangd] Add textDocument/ast extension method to dump the AST

Sam McCall via cfe-commits cfe-commits at lists.llvm.org
Thu Nov 19 16:15:27 PST 2020


Author: Sam McCall
Date: 2020-11-20T01:13:28+01:00
New Revision: 8adc4d1ec76471bc283d888f3077f7d8f591d6ad

URL: https://github.com/llvm/llvm-project/commit/8adc4d1ec76471bc283d888f3077f7d8f591d6ad
DIFF: https://github.com/llvm/llvm-project/commit/8adc4d1ec76471bc283d888f3077f7d8f591d6ad.diff

LOG: [clangd] Add textDocument/ast extension method to dump the AST

This is a mass-market version of the "dump AST" tweak we have behind
-hidden-features.
I think in this friendlier form it'll be useful for people outside clang
developers, which would justify making it a real feature.
It could be useful as a step towards lightweight clang-AST tooling in clangd
itself (like matcher-based search).

Advantages over the tweak:
 - simplified information makes it more accessible, likely somewhat useful
   without learning too much clang internals
 - can be shown in a tree view
 - structured information gives some options for presentation (e.g.
   icon + two text colors + tooltip in vscode)
 - clickable nodes jump to the corresponding code
Disadvantages:
 - a bunch of code to handle different node types
 - likely missing some important info vs dump-ast due to brevity/oversight
 - may end up chasing/maintaining support for the long tail of nodes

Demo with VSCode support: https://imgur.com/a/6gKfyIV

Differential Revision: https://reviews.llvm.org/D89571

Added: 
    clang-tools-extra/clangd/DumpAST.cpp
    clang-tools-extra/clangd/DumpAST.h
    clang-tools-extra/clangd/test/ast.test
    clang-tools-extra/clangd/unittests/DumpASTTests.cpp

Modified: 
    clang-tools-extra/clangd/CMakeLists.txt
    clang-tools-extra/clangd/ClangdLSPServer.cpp
    clang-tools-extra/clangd/ClangdLSPServer.h
    clang-tools-extra/clangd/ClangdServer.cpp
    clang-tools-extra/clangd/ClangdServer.h
    clang-tools-extra/clangd/Protocol.cpp
    clang-tools-extra/clangd/Protocol.h
    clang-tools-extra/clangd/test/initialize-params.test
    clang-tools-extra/clangd/unittests/CMakeLists.txt

Removed: 
    


################################################################################
diff  --git a/clang-tools-extra/clangd/CMakeLists.txt b/clang-tools-extra/clangd/CMakeLists.txt
index 2b324674c81f..3fd110e9e135 100644
--- a/clang-tools-extra/clangd/CMakeLists.txt
+++ b/clang-tools-extra/clangd/CMakeLists.txt
@@ -56,6 +56,7 @@ add_clang_library(clangDaemon
   ConfigYAML.cpp
   Diagnostics.cpp
   DraftStore.cpp
+  DumpAST.cpp
   ExpectedTypes.cpp
   FindSymbols.cpp
   FindTarget.cpp

diff  --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index 3164b6cbfb14..e726271fe7cb 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -11,6 +11,7 @@
 #include "CodeComplete.h"
 #include "Diagnostics.h"
 #include "DraftStore.h"
+#include "DumpAST.h"
 #include "GlobalCompilationDatabase.h"
 #include "Protocol.h"
 #include "SemanticHighlighting.h"
@@ -21,6 +22,7 @@
 #include "support/Context.h"
 #include "support/MemoryTree.h"
 #include "support/Trace.h"
+#include "clang/AST/ASTContext.h"
 #include "clang/Basic/Version.h"
 #include "clang/Tooling/Core/Replacement.h"
 #include "llvm/ADT/ArrayRef.h"
@@ -613,6 +615,7 @@ void ClangdLSPServer::onInitialize(const InitializeParams &Params,
             {"documentSymbolProvider", true},
             {"workspaceSymbolProvider", true},
             {"referencesProvider", true},
+            {"astProvider", true},
             {"executeCommandProvider",
              llvm::json::Object{
                  {"commands",
@@ -1403,6 +1406,11 @@ void ClangdLSPServer::onMemoryUsage(const NoParams &,
   Reply(std::move(MT));
 }
 
+void ClangdLSPServer::onAST(const ASTParams &Params,
+                            Callback<llvm::Optional<ASTNode>> CB) {
+  Server->getAST(Params.textDocument.uri.file(), Params.range, std::move(CB));
+}
+
 ClangdLSPServer::ClangdLSPServer(class Transport &Transp,
                                  const ThreadsafeFS &TFS,
                                  const ClangdLSPServer::Options &Opts)
@@ -1432,6 +1440,7 @@ ClangdLSPServer::ClangdLSPServer(class Transport &Transp,
   MsgHandler->bind("workspace/executeCommand", &ClangdLSPServer::onCommand);
   MsgHandler->bind("textDocument/documentHighlight", &ClangdLSPServer::onDocumentHighlight);
   MsgHandler->bind("workspace/symbol", &ClangdLSPServer::onWorkspaceSymbol);
+  MsgHandler->bind("textDocument/ast", &ClangdLSPServer::onAST);
   MsgHandler->bind("textDocument/didOpen", &ClangdLSPServer::onDocumentDidOpen);
   MsgHandler->bind("textDocument/didClose", &ClangdLSPServer::onDocumentDidClose);
   MsgHandler->bind("textDocument/didChange", &ClangdLSPServer::onDocumentDidChange);

diff  --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h
index 9fb23a07f6c5..b9200f6a2e1b 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.h
+++ b/clang-tools-extra/clangd/ClangdLSPServer.h
@@ -93,6 +93,7 @@ class ClangdLSPServer : private ClangdServer::Callbacks {
   void onDocumentDidChange(const DidChangeTextDocumentParams &);
   void onDocumentDidClose(const DidCloseTextDocumentParams &);
   void onDocumentDidSave(const DidSaveTextDocumentParams &);
+  void onAST(const ASTParams &, Callback<llvm::Optional<ASTNode>>);
   void onDocumentOnTypeFormatting(const DocumentOnTypeFormattingParams &,
                                   Callback<std::vector<TextEdit>>);
   void onDocumentRangeFormatting(const DocumentRangeFormattingParams &,

diff  --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp
index 41c26be970d0..b6f9fcfd23da 100644
--- a/clang-tools-extra/clangd/ClangdServer.cpp
+++ b/clang-tools-extra/clangd/ClangdServer.cpp
@@ -9,6 +9,7 @@
 #include "ClangdServer.h"
 #include "CodeComplete.h"
 #include "Config.h"
+#include "DumpAST.h"
 #include "FindSymbols.h"
 #include "Format.h"
 #include "HeaderSourceSwitch.h"
@@ -784,6 +785,38 @@ void ClangdServer::semanticHighlights(
                            TUScheduler::InvalidateOnUpdate);
 }
 
+void ClangdServer::getAST(PathRef File, Range R,
+                          Callback<llvm::Optional<ASTNode>> CB) {
+  auto Action =
+      [R, CB(std::move(CB))](llvm::Expected<InputsAndAST> Inputs) mutable {
+        if (!Inputs)
+          return CB(Inputs.takeError());
+        unsigned Start, End;
+        if (auto Offset = positionToOffset(Inputs->Inputs.Contents, R.start))
+          Start = *Offset;
+        else
+          return CB(Offset.takeError());
+        if (auto Offset = positionToOffset(Inputs->Inputs.Contents, R.end))
+          End = *Offset;
+        else
+          return CB(Offset.takeError());
+
+        bool Success = SelectionTree::createEach(
+            Inputs->AST.getASTContext(), Inputs->AST.getTokens(), Start, End,
+            [&](SelectionTree T) {
+              if (const SelectionTree::Node *N = T.commonAncestor()) {
+                CB(dumpAST(N->ASTNode, Inputs->AST.getTokens(),
+                           Inputs->AST.getASTContext()));
+                return true;
+              }
+              return false;
+            });
+        if (!Success)
+          CB(llvm::None);
+      };
+  WorkScheduler.runWithAST("GetAST", File, std::move(Action));
+}
+
 void ClangdServer::customAction(PathRef File, llvm::StringRef Name,
                                 Callback<InputsAndAST> Action) {
   WorkScheduler.runWithAST(Name, File, std::move(Action));

diff  --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h
index 612524aae98d..0056f5072cca 100644
--- a/clang-tools-extra/clangd/ClangdServer.h
+++ b/clang-tools-extra/clangd/ClangdServer.h
@@ -319,6 +319,9 @@ class ClangdServer {
   void semanticHighlights(PathRef File,
                           Callback<std::vector<HighlightingToken>>);
 
+  /// Describe the AST subtree for a piece of code.
+  void getAST(PathRef File, Range R, Callback<llvm::Optional<ASTNode>> CB);
+
   /// Runs an arbitrary action that has access to the AST of the specified file.
   /// The action will execute on one of ClangdServer's internal threads.
   /// The AST is only valid for the duration of the callback.

diff  --git a/clang-tools-extra/clangd/DumpAST.cpp b/clang-tools-extra/clangd/DumpAST.cpp
new file mode 100644
index 000000000000..f9371ecf6374
--- /dev/null
+++ b/clang-tools-extra/clangd/DumpAST.cpp
@@ -0,0 +1,419 @@
+//===--- DumpAST.cpp - Serialize clang AST to LSP -------------------------===//
+//
+// 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 "DumpAST.h"
+#include "Protocol.h"
+#include "SourceCode.h"
+#include "support/Logger.h"
+#include "clang/AST/ASTTypeTraits.h"
+#include "clang/AST/Expr.h"
+#include "clang/AST/ExprCXX.h"
+#include "clang/AST/ExternalASTSource.h"
+#include "clang/AST/NestedNameSpecifier.h"
+#include "clang/AST/PrettyPrinter.h"
+#include "clang/AST/RecursiveASTVisitor.h"
+#include "clang/AST/TextNodeDumper.h"
+#include "clang/AST/Type.h"
+#include "clang/AST/TypeLoc.h"
+#include "clang/Basic/Specifiers.h"
+#include "clang/Tooling/Syntax/Tokens.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/raw_ostream.h"
+
+namespace clang {
+namespace clangd {
+namespace {
+
+using llvm::raw_ostream;
+template <typename Print> std::string toString(const Print &C) {
+  std::string Result;
+  llvm::raw_string_ostream OS(Result);
+  C(OS);
+  return std::move(OS.str());
+}
+
+bool isInjectedClassName(Decl *D) {
+  if (const auto *CRD = llvm::dyn_cast<CXXRecordDecl>(D))
+    return CRD->isInjectedClassName();
+  return false;
+}
+
+class DumpVisitor : public RecursiveASTVisitor<DumpVisitor> {
+  using Base = RecursiveASTVisitor<DumpVisitor>;
+
+  const syntax::TokenBuffer &Tokens;
+  const ASTContext &Ctx;
+
+  // Pointers are into 'children' vector.
+  // They remain valid because while a node is on the stack we only add
+  // descendants, not siblings.
+  std::vector<ASTNode *> Stack;
+
+  // Generic logic used to handle traversal of all node kinds.
+
+  template <typename T>
+  bool traverseNodePre(llvm::StringRef Role, const T &Node) {
+    if (Stack.empty()) {
+      assert(Root.role.empty());
+      Stack.push_back(&Root);
+    } else {
+      Stack.back()->children.emplace_back();
+      Stack.push_back(&Stack.back()->children.back());
+    }
+    auto &N = *Stack.back();
+    N.role = Role.str();
+    N.kind = getKind(Node);
+    N.detail = getDetail(Node);
+    N.range = getRange(Node);
+    N.arcana = getArcana(Node);
+    return true;
+  }
+  bool traverseNodePost() {
+    assert(!Stack.empty());
+    Stack.pop_back();
+    return true;
+  }
+  template <typename T, typename Callable>
+  bool traverseNode(llvm::StringRef Role, const T &Node, const Callable &Body) {
+    traverseNodePre(Role, Node);
+    Body();
+    return traverseNodePost();
+  }
+
+  // Range: most nodes have getSourceRange(), with a couple of exceptions.
+  // We only return it if it's valid at both ends and there are no macros.
+
+  template <typename T> llvm::Optional<Range> getRange(const T &Node) {
+    SourceRange SR = getSourceRange(Node);
+    auto Spelled = Tokens.spelledForExpanded(Tokens.expandedTokens(SR));
+    if (!Spelled)
+      return llvm::None;
+    return halfOpenToRange(
+        Tokens.sourceManager(),
+        CharSourceRange::getCharRange(Spelled->front().location(),
+                                      Spelled->back().endLocation()));
+  }
+  template <typename T, typename = decltype(std::declval<T>().getSourceRange())>
+  SourceRange getSourceRange(const T &Node) {
+    return Node.getSourceRange();
+  }
+  template <typename T,
+            typename = decltype(std::declval<T *>()->getSourceRange())>
+  SourceRange getSourceRange(const T *Node) {
+    return Node->getSourceRange();
+  }
+  // TemplateName doesn't have a real Loc node type.
+  SourceRange getSourceRange(const TemplateName &Node) { return SourceRange(); }
+  // Attr just uses a weird method name. Maybe we should fix it instead?
+  SourceRange getSourceRange(const Attr *Node) { return Node->getRange(); }
+
+  // Kind is usualy the class name, without the suffix ("Type" etc).
+  // Where there's a set of variants instead, we use the 'Kind' enum values.
+
+  std::string getKind(const Decl *D) { return D->getDeclKindName(); }
+  std::string getKind(const Stmt *S) {
+    std::string Result = S->getStmtClassName();
+    if (llvm::StringRef(Result).endswith("Stmt") ||
+        llvm::StringRef(Result).endswith("Expr"))
+      Result.resize(Result.size() - 4);
+    return Result;
+  }
+  std::string getKind(const TypeLoc &TL) {
+    std::string Result;
+    if (TL.getTypeLocClass() == TypeLoc::Qualified)
+      return "Qualified";
+    return TL.getType()->getTypeClassName();
+  }
+  std::string getKind(const TemplateArgumentLoc &TAL) {
+    switch (TAL.getArgument().getKind()) {
+#define TEMPLATE_ARGUMENT_KIND(X)                                              \
+  case TemplateArgument::X:                                                    \
+    return #X
+      TEMPLATE_ARGUMENT_KIND(Null);
+      TEMPLATE_ARGUMENT_KIND(NullPtr);
+      TEMPLATE_ARGUMENT_KIND(Expression);
+      TEMPLATE_ARGUMENT_KIND(Integral);
+      TEMPLATE_ARGUMENT_KIND(Pack);
+      TEMPLATE_ARGUMENT_KIND(Type);
+      TEMPLATE_ARGUMENT_KIND(Declaration);
+      TEMPLATE_ARGUMENT_KIND(Template);
+      TEMPLATE_ARGUMENT_KIND(TemplateExpansion);
+#undef TEMPLATE_ARGUMENT_KIND
+    }
+  }
+  std::string getKind(const NestedNameSpecifierLoc &NNSL) {
+    assert(NNSL.getNestedNameSpecifier());
+    switch (NNSL.getNestedNameSpecifier()->getKind()) {
+#define NNS_KIND(X)                                                            \
+  case NestedNameSpecifier::X:                                                 \
+    return #X
+      NNS_KIND(Identifier);
+      NNS_KIND(Namespace);
+      NNS_KIND(TypeSpec);
+      NNS_KIND(TypeSpecWithTemplate);
+      NNS_KIND(Global);
+      NNS_KIND(Super);
+      NNS_KIND(NamespaceAlias);
+#undef NNS_KIND
+    }
+  }
+  std::string getKind(const CXXCtorInitializer *CCI) {
+    if (CCI->isBaseInitializer())
+      return "BaseInitializer";
+    if (CCI->isDelegatingInitializer())
+      return "DelegatingInitializer";
+    if (CCI->isAnyMemberInitializer())
+      return "MemberInitializer";
+    llvm_unreachable("Unhandled CXXCtorInitializer type");
+  }
+  std::string getKind(const TemplateName &TN) {
+    switch (TN.getKind()) {
+#define TEMPLATE_KIND(X)                                                       \
+  case TemplateName::X:                                                        \
+    return #X;
+      TEMPLATE_KIND(Template);
+      TEMPLATE_KIND(OverloadedTemplate);
+      TEMPLATE_KIND(AssumedTemplate);
+      TEMPLATE_KIND(QualifiedTemplate);
+      TEMPLATE_KIND(DependentTemplate);
+      TEMPLATE_KIND(SubstTemplateTemplateParm);
+      TEMPLATE_KIND(SubstTemplateTemplateParmPack);
+#undef TEMPLATE_KIND
+    }
+  }
+  std::string getKind(const Attr *A) {
+    switch (A->getKind()) {
+#define ATTR(X)                                                                \
+  case attr::X:                                                                \
+    return #X;
+#include "clang/Basic/AttrList.inc"
+#undef ATTR
+    }
+  }
+  std::string getKind(const CXXBaseSpecifier &CBS) {
+    // There aren't really any variants of CXXBaseSpecifier.
+    // To avoid special cases in the API/UI, use public/private as the kind.
+    return getAccessSpelling(CBS.getAccessSpecifier()).str();
+  }
+
+  // Detail is the single most important fact about the node.
+  // Often this is the name, sometimes a "kind" enum like operators or casts.
+  // We should avoid unbounded text, like dumping parameter lists.
+
+  std::string getDetail(const Decl *D) {
+    const auto *ND = dyn_cast<NamedDecl>(D);
+    if (!ND || llvm::isa_and_nonnull<CXXConstructorDecl>(ND->getAsFunction()) ||
+        isa<CXXDestructorDecl>(ND))
+      return "";
+    std::string Name = toString([&](raw_ostream &OS) { ND->printName(OS); });
+    if (Name.empty())
+      return "(anonymous)";
+    return Name;
+  }
+  std::string getDetail(const Stmt *S) {
+    if (const auto *DRE = dyn_cast<DeclRefExpr>(S))
+      return DRE->getNameInfo().getAsString();
+    if (const auto *DSDRE = dyn_cast<DependentScopeDeclRefExpr>(S))
+      return DSDRE->getNameInfo().getAsString();
+    if (const auto *ME = dyn_cast<MemberExpr>(S))
+      return ME->getMemberNameInfo().getAsString();
+    if (const auto *CE = dyn_cast<CastExpr>(S))
+      return CE->getCastKindName();
+    if (const auto *BO = dyn_cast<BinaryOperator>(S))
+      return BO->getOpcodeStr().str();
+    if (const auto *UO = dyn_cast<UnaryOperator>(S))
+      return UnaryOperator::getOpcodeStr(UO->getOpcode()).str();
+    if (const auto *CCO = dyn_cast<CXXConstructExpr>(S))
+      return CCO->getConstructor()->getNameAsString();
+    if (isa<IntegerLiteral>(S) || isa<FloatingLiteral>(S) ||
+        isa<FixedPointLiteral>(S) || isa<CharacterLiteral>(S) ||
+        isa<ImaginaryLiteral>(S) || isa<CXXBoolLiteralExpr>(S))
+      return toString([&](raw_ostream &OS) {
+        S->printPretty(OS, nullptr, Ctx.getPrintingPolicy());
+      });
+    if (const auto *MTE = dyn_cast<MaterializeTemporaryExpr>(S))
+      return MTE->isBoundToLvalueReference() ? "lvalue" : "rvalue";
+    return "";
+  }
+  std::string getDetail(const TypeLoc &TL) {
+    if (TL.getType().hasLocalQualifiers())
+      return TL.getType().getLocalQualifiers().getAsString(
+          Ctx.getPrintingPolicy());
+    if (const auto *TT = dyn_cast<TagType>(TL.getTypePtr()))
+      return getDetail(TT->getDecl());
+    if (const auto *DT = dyn_cast<DeducedType>(TL.getTypePtr()))
+      if (DT->isDeduced())
+        return DT->getDeducedType().getAsString(Ctx.getPrintingPolicy());
+    if (const auto *BT = dyn_cast<BuiltinType>(TL.getTypePtr()))
+      return BT->getName(Ctx.getPrintingPolicy()).str();
+    if (const auto *TTPT = dyn_cast<TemplateTypeParmType>(TL.getTypePtr()))
+      return getDetail(TTPT->getDecl());
+    if (const auto *TT = dyn_cast<TypedefType>(TL.getTypePtr()))
+      return getDetail(TT->getDecl());
+    return "";
+  }
+  std::string getDetail(const NestedNameSpecifierLoc &NNSL) {
+    const auto &NNS = *NNSL.getNestedNameSpecifier();
+    switch (NNS.getKind()) {
+    case NestedNameSpecifier::Identifier:
+      return NNS.getAsIdentifier()->getName().str() + "::";
+    case NestedNameSpecifier::Namespace:
+      return NNS.getAsNamespace()->getNameAsString() + "::";
+    case NestedNameSpecifier::NamespaceAlias:
+      return NNS.getAsNamespaceAlias()->getNameAsString() + "::";
+    default:
+      return "";
+    }
+  }
+  std::string getDetail(const CXXCtorInitializer *CCI) {
+    if (FieldDecl *FD = CCI->getAnyMember())
+      return getDetail(FD);
+    if (TypeLoc TL = CCI->getBaseClassLoc())
+      return getDetail(TL);
+    return "";
+  }
+  std::string getDetail(const TemplateArgumentLoc &TAL) {
+    if (TAL.getArgument().getKind() == TemplateArgument::Integral)
+      return TAL.getArgument().getAsIntegral().toString(10);
+    return "";
+  }
+  std::string getDetail(const TemplateName &TN) {
+    return toString([&](raw_ostream &OS) {
+      TN.print(OS, Ctx.getPrintingPolicy(), /*SuppressNNS=*/true);
+    });
+  }
+  std::string getDetail(const Attr *A) {
+    return A->getAttrName() ? A->getNormalizedFullName() : A->getSpelling();
+  }
+  std::string getDetail(const CXXBaseSpecifier &CBS) {
+    return CBS.isVirtual() ? "virtual" : "";
+  }
+
+  /// Arcana is produced by TextNodeDumper, for the types it supports.
+
+  template <typename Dump> std::string dump(const Dump &D) {
+    return toString([&](raw_ostream &OS) {
+      TextNodeDumper Dumper(OS, Ctx, /*ShowColors=*/false);
+      D(Dumper);
+    });
+  }
+  template <typename T> std::string getArcana(const T &N) {
+    return dump([&](TextNodeDumper &D) { D.Visit(N); });
+  }
+  std::string getArcana(const NestedNameSpecifierLoc &NNS) { return ""; }
+  std::string getArcana(const TemplateName &NNS) { return ""; }
+  std::string getArcana(const CXXBaseSpecifier &CBS) { return ""; }
+  std::string getArcana(const TemplateArgumentLoc &TAL) {
+    return dump([&](TextNodeDumper &D) {
+      D.Visit(TAL.getArgument(), TAL.getSourceRange());
+    });
+  }
+  std::string getArcana(const TypeLoc &TL) {
+    return dump([&](TextNodeDumper &D) { D.Visit(TL.getType()); });
+  }
+
+public:
+  ASTNode Root;
+  DumpVisitor(const syntax::TokenBuffer &Tokens, const ASTContext &Ctx)
+      : Tokens(Tokens), Ctx(Ctx) {}
+
+  // Override traversal to record the nodes we care about.
+  // Generally, these are nodes with position information (TypeLoc, not Type).
+  bool TraverseDecl(Decl *D) {
+    return !D || isInjectedClassName(D) ||
+           traverseNode("declaration", D, [&] { Base::TraverseDecl(D); });
+  }
+  bool TraverseTypeLoc(TypeLoc TL) {
+    return !TL || traverseNode("type", TL, [&] { Base::TraverseTypeLoc(TL); });
+  }
+  bool TraverseTemplateName(const TemplateName &TN) {
+    return traverseNode("template name", TN,
+                        [&] { Base::TraverseTemplateName(TN); });
+  }
+  bool TraverseTemplateArgumentLoc(const TemplateArgumentLoc &TAL) {
+    return traverseNode("template argument", TAL,
+                        [&] { Base::TraverseTemplateArgumentLoc(TAL); });
+  }
+  bool TraverseNestedNameSpecifierLoc(NestedNameSpecifierLoc NNSL) {
+    return !NNSL || traverseNode("specifier", NNSL, [&] {
+      Base::TraverseNestedNameSpecifierLoc(NNSL);
+    });
+  }
+  bool TraverseConstructorInitializer(CXXCtorInitializer *CCI) {
+    return !CCI || traverseNode("constructor initializer", CCI, [&] {
+      Base::TraverseConstructorInitializer(CCI);
+    });
+  }
+  bool TraverseAttr(Attr *A) {
+    return !A || traverseNode("attribute", A, [&] { Base::TraverseAttr(A); });
+  }
+  bool TraverseCXXBaseSpecifier(const CXXBaseSpecifier &CBS) {
+    return traverseNode("base", CBS,
+                        [&] { Base::TraverseCXXBaseSpecifier(CBS); });
+  }
+  // Stmt is the same, but this form allows the data recursion optimization.
+  bool dataTraverseStmtPre(Stmt *S) {
+    return S && traverseNodePre(isa<Expr>(S) ? "expression" : "statement", S);
+  }
+  bool dataTraverseStmtPost(Stmt *X) { return traverseNodePost(); }
+
+  // QualifiedTypeLoc is handled strangely in RecursiveASTVisitor: the derived
+  // TraverseTypeLoc is not called for the inner UnqualTypeLoc.
+  // This means we'd never see 'int' in 'const int'! Work around that here.
+  // (The reason for the behavior is to avoid traversing the nested Type twice,
+  // but we ignore TraverseType anyway).
+  bool TraverseQualifiedTypeLoc(QualifiedTypeLoc QTL) {
+    return TraverseTypeLoc(QTL.getUnqualifiedLoc());
+  }
+  // Uninteresting parts of the AST that don't have locations within them.
+  bool TraverseNestedNameSpecifier(NestedNameSpecifier *) { return true; }
+  bool TraverseType(QualType) { return true; }
+
+  // OpaqueValueExpr blocks traversal, we must explicitly traverse it.
+  bool TraverseOpaqueValueExpr(OpaqueValueExpr *E) {
+    return TraverseStmt(E->getSourceExpr());
+  }
+  // We only want to traverse the *syntactic form* to understand the selection.
+  bool TraversePseudoObjectExpr(PseudoObjectExpr *E) {
+    return TraverseStmt(E->getSyntacticForm());
+  }
+};
+
+} // namespace
+
+ASTNode dumpAST(const DynTypedNode &N, const syntax::TokenBuffer &Tokens,
+                const ASTContext &Ctx) {
+  DumpVisitor V(Tokens, Ctx);
+  // DynTypedNode only works with const, RecursiveASTVisitor only non-const :-(
+  if (const auto *D = N.get<Decl>())
+    V.TraverseDecl(const_cast<Decl *>(D));
+  else if (const auto *S = N.get<Stmt>())
+    V.TraverseStmt(const_cast<Stmt *>(S));
+  else if (const auto *NNSL = N.get<NestedNameSpecifierLoc>())
+    V.TraverseNestedNameSpecifierLoc(
+        *const_cast<NestedNameSpecifierLoc *>(NNSL));
+  else if (const auto *NNS = N.get<NestedNameSpecifier>())
+    V.TraverseNestedNameSpecifier(const_cast<NestedNameSpecifier *>(NNS));
+  else if (const auto *TL = N.get<TypeLoc>())
+    V.TraverseTypeLoc(*const_cast<TypeLoc *>(TL));
+  else if (const auto *QT = N.get<QualType>())
+    V.TraverseType(*const_cast<QualType *>(QT));
+  else if (const auto *CCI = N.get<CXXCtorInitializer>())
+    V.TraverseConstructorInitializer(const_cast<CXXCtorInitializer *>(CCI));
+  else if (const auto *TAL = N.get<TemplateArgumentLoc>())
+    V.TraverseTemplateArgumentLoc(*const_cast<TemplateArgumentLoc *>(TAL));
+  else if (const auto *CBS = N.get<CXXBaseSpecifier>())
+    V.TraverseCXXBaseSpecifier(*const_cast<CXXBaseSpecifier *>(CBS));
+  else
+    elog("dumpAST: unhandled DynTypedNode kind {0}",
+         N.getNodeKind().asStringRef());
+  return std::move(V.Root);
+}
+
+} // namespace clangd
+} // namespace clang

diff  --git a/clang-tools-extra/clangd/DumpAST.h b/clang-tools-extra/clangd/DumpAST.h
new file mode 100644
index 000000000000..968ccbfabec6
--- /dev/null
+++ b/clang-tools-extra/clangd/DumpAST.h
@@ -0,0 +1,48 @@
+//===--- DumpAST.h - Serialize clang AST to LSP -----------------*- 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Exposing clang's AST can give insight into the precise meaning of code.
+// (C++ is a complicated language, and very few people know all its rules).
+// Despite the name, clang's AST describes *semantics* and so includes nodes
+// for implicit behavior like conversions.
+//
+// It's also useful to developers who work with the clang AST specifically,
+// and want to know how certain constructs are represented.
+//
+// The main representation is not based on the familiar -ast-dump output,
+// which is heavy on internal details.
+// It also does not use the -ast-dump=json output, which captures the same
+// detail in a machine-friendly way, but requires client-side logic to present.
+// Instead, the key information is bundled into a few fields (role/kind/detail)
+// with weakly-defined semantics, optimized for easy presentation.
+// The -ast-dump output is preserved in the 'arcana' field, and may be shown
+// e.g. as a tooltip.
+//
+// The textDocument/ast method implemented here is a clangd extension.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_DUMPAST_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_DUMPAST_H
+
+#include "Protocol.h"
+#include "clang/AST/ASTContext.h"
+
+namespace clang {
+namespace syntax {
+class TokenBuffer;
+} // namespace syntax
+namespace clangd {
+
+ASTNode dumpAST(const DynTypedNode &, const syntax::TokenBuffer &Tokens,
+                const ASTContext &);
+
+} // namespace clangd
+} // namespace clang
+
+#endif

diff  --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp
index 7464485e7058..78110dc0de60 100644
--- a/clang-tools-extra/clangd/Protocol.cpp
+++ b/clang-tools-extra/clangd/Protocol.cpp
@@ -1370,5 +1370,42 @@ llvm::json::Value toJSON(const MemoryTree &MT) {
   Out["_total"] = Total;
   return Out;
 }
+
+bool fromJSON(const llvm::json::Value &Params, ASTParams &R,
+              llvm::json::Path P) {
+  llvm::json::ObjectMapper O(Params, P);
+  return O && O.map("textDocument", R.textDocument) && O.map("range", R.range);
+}
+
+llvm::json::Value toJSON(const ASTNode &N) {
+  llvm::json::Object Result{
+      {"role", N.role},
+      {"kind", N.kind},
+  };
+  if (!N.children.empty())
+    Result["children"] = N.children;
+  if (!N.detail.empty())
+    Result["detail"] = N.detail;
+  if (!N.arcana.empty())
+    Result["arcana"] = N.arcana;
+  if (N.range)
+    Result["range"] = *N.range;
+  return Result;
+}
+
+llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const ASTNode &Root) {
+  std::function<void(const ASTNode &, unsigned)> Print = [&](const ASTNode &N,
+                                                             unsigned Level) {
+    OS.indent(2 * Level) << N.role << ": " << N.kind;
+    if (!N.detail.empty())
+      OS << " - " << N.detail;
+    OS << "\n";
+    for (const ASTNode &C : N.children)
+      Print(C, Level + 1);
+  };
+  Print(Root, 0);
+  return OS;
+}
+
 } // namespace clangd
 } // namespace clang

diff  --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h
index 8d2f5d2e15b0..9f0e50fe863f 100644
--- a/clang-tools-extra/clangd/Protocol.h
+++ b/clang-tools-extra/clangd/Protocol.h
@@ -1686,6 +1686,45 @@ llvm::json::Value toJSON(const FoldingRange &Range);
 ///   }
 llvm::json::Value toJSON(const MemoryTree &MT);
 
+/// Payload for textDocument/ast request.
+/// This request is a clangd extension.
+struct ASTParams {
+  /// The text document.
+  TextDocumentIdentifier textDocument;
+
+  /// The position of the node to be dumped.
+  /// The highest-level node that entirely contains the range will be returned.
+  Range range;
+};
+bool fromJSON(const llvm::json::Value &, ASTParams &, llvm::json::Path);
+
+/// Simplified description of a clang AST node.
+/// This is clangd's internal representation of C++ code.
+struct ASTNode {
+  /// The general kind of node, such as "expression"
+  /// Corresponds to the base AST node type such as Expr.
+  std::string role;
+  /// The specific kind of node this is, such as "BinaryOperator".
+  /// This is usually a concrete node class (with Expr etc suffix dropped).
+  /// When there's no hierarchy (e.g. TemplateName), the variant (NameKind).
+  std::string kind;
+  /// Brief additional information, such as "||" for the particular operator.
+  /// The information included depends on the node kind, and may be empty.
+  std::string detail;
+  /// A one-line dump of detailed information about the node.
+  /// This includes role/kind/description information, but is rather cryptic.
+  /// It is similar to the output from `clang -Xclang -ast-dump`.
+  /// May be empty for certain types of nodes.
+  std::string arcana;
+  /// The range of the original source file covered by this node.
+  /// May be missing for implicit nodes, or those created by macro expansion.
+  llvm::Optional<Range> range;
+  /// Nodes nested within this one, such as the operands of a BinaryOperator.
+  std::vector<ASTNode> children;
+};
+llvm::json::Value toJSON(const ASTNode &);
+llvm::raw_ostream &operator<<(llvm::raw_ostream &, const ASTNode &);
+
 } // namespace clangd
 } // namespace clang
 

diff  --git a/clang-tools-extra/clangd/test/ast.test b/clang-tools-extra/clangd/test/ast.test
new file mode 100644
index 000000000000..2185f14eefb8
--- /dev/null
+++ b/clang-tools-extra/clangd/test/ast.test
@@ -0,0 +1,49 @@
+# RUN: clangd -lit-test < %s | FileCheck %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:///simple.cpp","languageId":"cpp","version":1,"text":"int x;"}}}
+---
+{"jsonrpc":"2.0","id":1,"method":"textDocument/ast","params":{
+  "textDocument":{"uri":"test:///simple.cpp"},
+  "range": {"start": {"line":0, "character":0}, "end": {"line":0, "character":5}}
+}}
+#      CHECK:  "id": 1,
+# CHECK-NEXT:  "jsonrpc": "2.0",
+# CHECK-NEXT:  "result": {
+# CHECK-NEXT:    "arcana": "VarDecl {{.*}} x 'int'",
+# CHECK-NEXT:    "children": [
+# CHECK-NEXT:      {
+# CHECK-NEXT:        "arcana": "QualType {{.*}} 'int' ",
+# CHECK-NEXT:        "detail": "int",
+# CHECK-NEXT:        "kind": "Builtin",
+# CHECK-NEXT:        "range": {
+# CHECK-NEXT:          "end": {
+# CHECK-NEXT:            "character": 3,
+# CHECK-NEXT:            "line": 0
+# CHECK-NEXT:          },
+# CHECK-NEXT:          "start": {
+# CHECK-NEXT:            "character": 0,
+# CHECK-NEXT:            "line": 0
+# CHECK-NEXT:          }
+# CHECK-NEXT:        },
+# CHECK-NEXT:        "role": "type"
+# CHECK-NEXT:      }
+# CHECK-NEXT:    ],
+# CHECK-NEXT:    "detail": "x",
+# CHECK-NEXT:    "kind": "Var",
+# CHECK-NEXT:    "range": {
+# CHECK-NEXT:      "end": {
+# CHECK-NEXT:        "character": 5,
+# CHECK-NEXT:        "line": 0
+# CHECK-NEXT:      },
+# CHECK-NEXT:      "start": {
+# CHECK-NEXT:        "character": 0,
+# CHECK-NEXT:        "line": 0
+# CHECK-NEXT:      }
+# CHECK-NEXT:    },
+# CHECK-NEXT:    "role": "declaration"
+# CHECK-NEXT:  }
+---
+{"jsonrpc":"2.0","id":2,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}

diff  --git a/clang-tools-extra/clangd/test/initialize-params.test b/clang-tools-extra/clangd/test/initialize-params.test
index 8f4d0653bb78..a08a9b14a317 100644
--- a/clang-tools-extra/clangd/test/initialize-params.test
+++ b/clang-tools-extra/clangd/test/initialize-params.test
@@ -5,6 +5,7 @@
 # CHECK-NEXT:  "jsonrpc": "2.0",
 # CHECK-NEXT:  "result": {
 # CHECK-NEXT:    "capabilities": {
+# CHECK-NEXT:      "astProvider": true,
 # CHECK-NEXT:      "codeActionProvider": true,
 # CHECK-NEXT:      "completionProvider": {
 # CHECK-NEXT:        "allCommitCharacters": [

diff  --git a/clang-tools-extra/clangd/unittests/CMakeLists.txt b/clang-tools-extra/clangd/unittests/CMakeLists.txt
index bf964484bc6c..e570072783f1 100644
--- a/clang-tools-extra/clangd/unittests/CMakeLists.txt
+++ b/clang-tools-extra/clangd/unittests/CMakeLists.txt
@@ -51,6 +51,7 @@ add_unittest(ClangdUnitTests ClangdTests
   DexTests.cpp
   DiagnosticsTests.cpp
   DraftStoreTests.cpp
+  DumpASTTests.cpp
   ExpectedTypeTest.cpp
   FileDistanceTests.cpp
   FileIndexTests.cpp

diff  --git a/clang-tools-extra/clangd/unittests/DumpASTTests.cpp b/clang-tools-extra/clangd/unittests/DumpASTTests.cpp
new file mode 100644
index 000000000000..4b524820cb4c
--- /dev/null
+++ b/clang-tools-extra/clangd/unittests/DumpASTTests.cpp
@@ -0,0 +1,151 @@
+//===-- DumpASTTests.cpp --------------------------------------------------===//
+//
+// 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 "Annotations.h"
+#include "DumpAST.h"
+#include "TestTU.h"
+#include "llvm/Support/ScopedPrinter.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace clang {
+namespace clangd {
+namespace {
+using testing::SizeIs;
+
+TEST(DumpASTTests, BasicInfo) {
+  std::pair</*Code=*/std::string, /*Expected=*/std::string> Cases[] = {
+      {R"cpp(
+float root(int *x) {
+  return *x + 1;
+}
+      )cpp",
+       R"(
+declaration: Function - root
+  type: FunctionProto
+    type: Builtin - float
+    declaration: ParmVar - x
+      type: Pointer
+        type: Builtin - int
+  statement: Compound
+    statement: Return
+      expression: ImplicitCast - IntegralToFloating
+        expression: BinaryOperator - +
+          expression: ImplicitCast - LValueToRValue
+            expression: UnaryOperator - *
+              expression: ImplicitCast - LValueToRValue
+                expression: DeclRef - x
+          expression: IntegerLiteral - 1
+      )"},
+      {R"cpp(
+namespace root {
+struct S { static const int x = 0; };
+int y = S::x + root::S().x;
+}
+      )cpp",
+       R"(
+declaration: Namespace - root
+  declaration: CXXRecord - S
+    declaration: Var - x
+      type: Qualified - const
+        type: Builtin - int
+      expression: IntegerLiteral - 0
+    declaration: CXXConstructor
+    declaration: CXXConstructor
+    declaration: CXXConstructor
+    declaration: CXXDestructor
+  declaration: Var - y
+    type: Builtin - int
+    expression: ExprWithCleanups
+      expression: BinaryOperator - +
+        expression: ImplicitCast - LValueToRValue
+          expression: DeclRef - x
+            specifier: TypeSpec
+              type: Record - S
+        expression: ImplicitCast - LValueToRValue
+          expression: Member - x
+            expression: MaterializeTemporary - rvalue
+              expression: CXXTemporaryObject - S
+                type: Elaborated
+                  specifier: Namespace - root::
+                  type: Record - S
+      )"},
+      {R"cpp(
+template <typename T> int root() {
+  (void)root<unsigned>();
+  return T::value;
+}
+      )cpp",
+       R"(
+declaration: FunctionTemplate - root
+  declaration: TemplateTypeParm - T
+  declaration: Function - root
+    type: FunctionProto
+      type: Builtin - int
+    statement: Compound
+      expression: CStyleCast - ToVoid
+        type: Builtin - void
+        expression: Call
+          expression: ImplicitCast - FunctionToPointerDecay
+            expression: DeclRef - root
+              template argument: Type
+                type: Builtin - unsigned int
+      statement: Return
+        expression: DependentScopeDeclRef - value
+          specifier: TypeSpec
+            type: TemplateTypeParm - T
+      )"},
+      {R"cpp(
+struct Foo { char operator+(int); };
+char root = Foo() + 42;
+      )cpp",
+       R"(
+declaration: Var - root
+  type: Builtin - char
+  expression: ExprWithCleanups
+    expression: CXXOperatorCall
+      expression: ImplicitCast - FunctionToPointerDecay
+        expression: DeclRef - operator+
+      expression: MaterializeTemporary - lvalue
+        expression: CXXTemporaryObject - Foo
+          type: Record - Foo
+      expression: IntegerLiteral - 42
+      )"},
+  };
+  for (const auto &Case : Cases) {
+    ParsedAST AST = TestTU::withCode(Case.first).build();
+    auto Node = dumpAST(DynTypedNode::create(findDecl(AST, "root")),
+                        AST.getTokens(), AST.getASTContext());
+    EXPECT_EQ(llvm::StringRef(Case.second).trim(),
+              llvm::StringRef(llvm::to_string(Node)).trim());
+  }
+}
+
+TEST(DumpASTTests, Range) {
+  Annotations Case("$var[[$type[[int]] x]];");
+  ParsedAST AST = TestTU::withCode(Case.code()).build();
+  auto Node = dumpAST(DynTypedNode::create(findDecl(AST, "x")), AST.getTokens(),
+                      AST.getASTContext());
+  EXPECT_EQ(Node.range, Case.range("var"));
+  ASSERT_THAT(Node.children, SizeIs(1)) << "Expected one child typeloc";
+  EXPECT_EQ(Node.children.front().range, Case.range("type"));
+}
+
+TEST(DumpASTTests, Arcana) {
+  ParsedAST AST = TestTU::withCode("int x;").build();
+  auto Node = dumpAST(DynTypedNode::create(findDecl(AST, "x")), AST.getTokens(),
+                      AST.getASTContext());
+  EXPECT_THAT(Node.arcana, testing::StartsWith("VarDecl "));
+  EXPECT_THAT(Node.arcana, testing::EndsWith(" 'int'"));
+  ASSERT_THAT(Node.children, SizeIs(1)) << "Expected one child typeloc";
+  EXPECT_THAT(Node.children.front().arcana, testing::StartsWith("QualType "));
+}
+
+} // namespace
+} // namespace clangd
+} // namespace clang


        


More information about the cfe-commits mailing list