[clang-tools-extra] [llvm] [Clangd] Add AST search capabilities from clang-query (PR #156090)

Fabian Keßler-Schulz via llvm-commits llvm-commits at lists.llvm.org
Thu Sep 4 09:53:14 PDT 2025


https://github.com/Febbe updated https://github.com/llvm/llvm-project/pull/156090

>From 6ed1d5446c11b5d3436d71fa1eb58648633bcb9a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fabian=20Ke=C3=9Fler?= <fabian_kessler at gmx.de>
Date: Wed, 2 Oct 2024 16:33:22 +0200
Subject: [PATCH 1/5] WIP

---
 clang-tools-extra/clangd/CMakeLists.txt      |  2 +
 clang-tools-extra/clangd/ClangdLSPServer.cpp | 14 +++++
 clang-tools-extra/clangd/ClangdLSPServer.h   |  1 +
 clang-tools-extra/clangd/ClangdServer.cpp    | 36 +++++++++++
 clang-tools-extra/clangd/ClangdServer.h      |  5 ++
 clang-tools-extra/clangd/Protocol.cpp        | 10 ++++
 clang-tools-extra/clangd/Protocol.h          | 13 ++++
 clang-tools-extra/clangd/XRefs.cpp           | 63 ++++++++++++++++++++
 clang-tools-extra/clangd/XRefs.h             | 10 ++++
 9 files changed, 154 insertions(+)

diff --git a/clang-tools-extra/clangd/CMakeLists.txt b/clang-tools-extra/clangd/CMakeLists.txt
index fb3f05329be21..06e655658645c 100644
--- a/clang-tools-extra/clangd/CMakeLists.txt
+++ b/clang-tools-extra/clangd/CMakeLists.txt
@@ -60,6 +60,7 @@ endif()
 
 include_directories(BEFORE "${CMAKE_CURRENT_BINARY_DIR}/../clang-tidy")
 include_directories(BEFORE "${CMAKE_CURRENT_SOURCE_DIR}/../include-cleaner/include")
+include_directories(BEFORE "${CMAKE_CURRENT_SOURCE_DIR}/../clang-query")
 
 add_clang_library(clangDaemon STATIC
   AST.cpp
@@ -183,6 +184,7 @@ target_link_libraries(clangDaemon
   ${LLVM_PTHREAD_LIB}
 
   clangIncludeCleaner
+  clangQuery
   clangTidy
   clangTidyUtils
 
diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index b445dcf2bbd2e..1330028d160f6 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -76,6 +76,7 @@ std::optional<int64_t> decodeVersion(llvm::StringRef Encoded) {
 const llvm::StringLiteral ApplyFixCommand = "clangd.applyFix";
 const llvm::StringLiteral ApplyTweakCommand = "clangd.applyTweak";
 const llvm::StringLiteral ApplyRenameCommand = "clangd.applyRename";
+constexpr llvm::StringLiteral SearchASTCommand = "clangd.searchAST";
 
 CodeAction toCodeAction(const ClangdServer::CodeActionResult::Rename &R,
                         const URIForFile &File) {
@@ -852,6 +853,18 @@ void ClangdLSPServer::onCommandApplyRename(const RenameParams &R,
   });
 }
 
+void ClangdLSPServer::onCommandSearchAST(const SearchASTArgs &Args,
+                                         Callback<llvm::json::Value> Reply) {
+  Server->findAST(Args, [Reply = std::move(Reply)](
+                            llvm::Expected<std::vector<std::vector<ASTNode>>>
+                                BoundNodes) mutable {
+    if (!BoundNodes)
+      return Reply(BoundNodes.takeError());
+    auto v = llvm::json::Value(*BoundNodes);
+    return Reply(*BoundNodes);
+  });
+}
+
 void ClangdLSPServer::applyEdit(WorkspaceEdit WE, llvm::json::Value Success,
                                 Callback<llvm::json::Value> Reply) {
   ApplyWorkspaceEditParams Edit;
@@ -1728,6 +1741,7 @@ void ClangdLSPServer::bindMethods(LSPBinder &Bind,
   Bind.command(ApplyFixCommand, this, &ClangdLSPServer::onCommandApplyEdit);
   Bind.command(ApplyTweakCommand, this, &ClangdLSPServer::onCommandApplyTweak);
   Bind.command(ApplyRenameCommand, this, &ClangdLSPServer::onCommandApplyRename);
+  Bind.command(SearchASTCommand, this, &ClangdLSPServer::onCommandSearchAST);
 
   ApplyWorkspaceEdit = Bind.outgoingMethod("workspace/applyEdit");
   PublishDiagnostics = Bind.outgoingNotification("textDocument/publishDiagnostics");
diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h
index 6ada3fd9e6e47..0daf3a4cc5220 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.h
+++ b/clang-tools-extra/clangd/ClangdLSPServer.h
@@ -186,6 +186,7 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
   void onCommandApplyEdit(const WorkspaceEdit &, Callback<llvm::json::Value>);
   void onCommandApplyTweak(const TweakArgs &, Callback<llvm::json::Value>);
   void onCommandApplyRename(const RenameParams &, Callback<llvm::json::Value>);
+  void onCommandSearchAST(const SearchASTArgs &, Callback<llvm::json::Value>);
 
   /// Outgoing LSP calls.
   LSPBinder::OutgoingMethod<ApplyWorkspaceEditParams,
diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp
index ac1e9aa5f0ff1..3d5b75f4511e4 100644
--- a/clang-tools-extra/clangd/ClangdServer.cpp
+++ b/clang-tools-extra/clangd/ClangdServer.cpp
@@ -810,6 +810,42 @@ void ClangdServer::locateSymbolAt(PathRef File, Position Pos,
   WorkScheduler->runWithAST("Definitions", File, std::move(Action));
 }
 
+void ClangdServer::findAST(SearchASTArgs const &Args,
+                           Callback<std::vector<std::vector<ASTNode>>> CB) {
+  auto Action =
+      [Args, CB = std::move(CB)](llvm::Expected<InputsAndAST> InpAST) mutable {
+        if (!InpAST)
+          return CB(InpAST.takeError());
+        auto BoundNodes = clangd::locateASTQuery(InpAST->AST, Args);
+        if (BoundNodes.empty())
+          return CB(error("No matching AST nodes found"));
+
+        auto &&AST = InpAST->AST;
+        // Convert BoundNodes to a vector of vectors to ASTNode's.
+        std::vector<std::vector<ASTNode>> Result;
+        Result.reserve(BoundNodes.size());
+        for (auto &&BN : BoundNodes) {
+          auto &&Map = BN.getMap();
+          std::vector<ASTNode> Nodes;
+          Nodes.reserve(Map.size());
+          for (const auto &[Key, Value] : Map) {
+            auto Node = dumpAST(Value, AST.getTokens(), AST.getASTContext());
+            Nodes.push_back(std::move(Node));
+          }
+          if (Nodes.empty())
+            continue;
+          Result.push_back(std::move(Nodes));
+        }
+        if (Result.empty()) {
+          return CB(error("No AST nodes found for the query"));
+        }
+        CB(std::move(Result));
+      };
+
+  WorkScheduler->runWithAST("Definitions", Args.textDocument.uri.file(),
+                            std::move(Action));
+}
+
 void ClangdServer::switchSourceHeader(
     PathRef Path, Callback<std::optional<clangd::Path>> CB) {
   // We want to return the result as fast as possible, strategy is:
diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h
index 4a1eae188f7eb..e2ee463d53b50 100644
--- a/clang-tools-extra/clangd/ClangdServer.h
+++ b/clang-tools-extra/clangd/ClangdServer.h
@@ -30,6 +30,8 @@
 #include "support/MemoryTree.h"
 #include "support/Path.h"
 #include "support/ThreadsafeFS.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
 #include "clang/Tooling/Core/Replacement.h"
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/FunctionExtras.h"
@@ -260,6 +262,9 @@ class ClangdServer {
   void locateSymbolAt(PathRef File, Position Pos,
                       Callback<std::vector<LocatedSymbol>> CB);
 
+  void findAST(const SearchASTArgs &Args,
+               Callback<std::vector<std::vector<ASTNode>>> CB);
+
   /// Switch to a corresponding source file when given a header file, and vice
   /// versa.
   void switchSourceHeader(PathRef Path,
diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp
index 2c858e28fa243..5012dc7edf584 100644
--- a/clang-tools-extra/clangd/Protocol.cpp
+++ b/clang-tools-extra/clangd/Protocol.cpp
@@ -13,6 +13,7 @@
 #include "Protocol.h"
 #include "URI.h"
 #include "support/Logger.h"
+#include "clang/AST/ASTTypeTraits.h"
 #include "clang/Basic/LLVM.h"
 #include "clang/Index/IndexSymbol.h"
 #include "llvm/ADT/StringExtras.h"
@@ -1650,6 +1651,15 @@ bool fromJSON(const llvm::json::Value &Params, SelectionRangeParams &S,
          O.map("positions", S.positions);
 }
 
+bool fromJSON(const llvm::json::Value &Params, SearchASTArgs &Args, llvm::json::Path P) {
+  llvm::json::ObjectMapper O(Params, P);
+  return O && O.map("query", Args.searchQuery)
+  && O.map("textDocument", Args.textDocument)
+  // && O.map("bindRoot", Args.bindRoot); TODO: add bindRoot to extend this feature
+  // && O.map("traversalKind", Args.tk); TODO: add traversalKind to extend this feature
+  ;
+}
+
 llvm::json::Value toJSON(const SelectionRange &Out) {
   if (Out.parent) {
     return llvm::json::Object{{"range", Out.range},
diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h
index 3a6bf155ee153..1d854f19a5eee 100644
--- a/clang-tools-extra/clangd/Protocol.h
+++ b/clang-tools-extra/clangd/Protocol.h
@@ -26,6 +26,7 @@
 #include "URI.h"
 #include "index/SymbolID.h"
 #include "support/MemoryTree.h"
+#include "clang/AST/ASTTypeTraits.h"
 #include "clang/Index/IndexSymbol.h"
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/Support/JSON.h"
@@ -1451,6 +1452,18 @@ struct RenameParams {
 bool fromJSON(const llvm::json::Value &, RenameParams &, llvm::json::Path);
 llvm::json::Value toJSON(const RenameParams &);
 
+struct SearchASTArgs {
+  std::string searchQuery;
+  TextDocumentIdentifier textDocument;
+
+  // Todo (extend feature): make them members and modifiable:
+  /// wheter the whole query is shown
+  static auto constexpr BindRoot = true;
+  /// Simplify things for users; default for now.
+  static auto constexpr Tk = TraversalKind::TK_IgnoreUnlessSpelledInSource;
+};
+bool fromJSON(const llvm::json::Value &, SearchASTArgs &, llvm::json::Path);
+
 struct PrepareRenameResult {
   /// Range of the string to rename.
   Range range;
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index a253a630a48cc..fcfa7ce42527e 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -14,6 +14,8 @@
 #include "ParsedAST.h"
 #include "Protocol.h"
 #include "Quality.h"
+#include "Query.h"
+#include "QuerySession.h"
 #include "Selection.h"
 #include "SourceCode.h"
 #include "URI.h"
@@ -41,6 +43,10 @@
 #include "clang/AST/StmtCXX.h"
 #include "clang/AST/StmtVisitor.h"
 #include "clang/AST/Type.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/ASTMatchers/Dynamic/Diagnostics.h"
+#include "clang/ASTMatchers/Dynamic/Parser.h"
 #include "clang/Basic/LLVM.h"
 #include "clang/Basic/LangOptions.h"
 #include "clang/Basic/SourceLocation.h"
@@ -52,6 +58,7 @@
 #include "clang/Index/IndexingOptions.h"
 #include "clang/Index/USRGeneration.h"
 #include "clang/Lex/Lexer.h"
+#include "clang/Parse/Parser.h"
 #include "clang/Sema/HeuristicResolver.h"
 #include "clang/Tooling/Syntax/Tokens.h"
 #include "llvm/ADT/ArrayRef.h"
@@ -66,6 +73,7 @@
 #include "llvm/Support/ErrorHandling.h"
 #include "llvm/Support/Path.h"
 #include "llvm/Support/raw_ostream.h"
+#include <cmath>
 #include <optional>
 #include <string>
 #include <vector>
@@ -773,6 +781,61 @@ const syntax::Token *findNearbyIdentifier(const SpelledWord &Word,
   return BestTok;
 }
 
+auto locateASTQuery(ParsedAST &AST, SearchASTArgs const &Query)
+    -> std::vector<ast_matchers::BoundNodes> {
+  using namespace ast_matchers;
+  using namespace ast_matchers::dynamic;
+  using ast_matchers::dynamic::Parser;
+
+  Diagnostics Diag;
+  auto MatcherSource = llvm::StringRef(Query.searchQuery).ltrim();
+  auto OrigMatcherSource = MatcherSource;
+
+  std::optional<DynTypedMatcher> Matcher = Parser::parseMatcherExpression(
+      MatcherSource,
+      nullptr /* is this sema instance usefull, to reduce overhead?*/,
+      nullptr /*we currently don't support let*/, &Diag);
+  if (!Matcher) {
+    elog("Not a valid top-level matcher.\n");
+    return {/* TODO */};
+  }
+  auto ActualSource = OrigMatcherSource.slice(0, OrigMatcherSource.size() -
+                                                     MatcherSource.size());
+  auto *Q = new query::MatchQuery(ActualSource, *Matcher);
+  Q->RemainingContent = MatcherSource;
+
+  // Q->run(AST);:
+  //==
+
+  struct CollectBoundNodes : MatchFinder::MatchCallback {
+    std::vector<BoundNodes> &Bindings;
+    CollectBoundNodes(std::vector<BoundNodes> &Bindings) : Bindings(Bindings) {}
+    void run(const MatchFinder::MatchResult &Result) override {
+      Bindings.push_back(Result.Nodes);
+    }
+  };
+
+  MatchFinder Finder;
+  std::vector<BoundNodes> Matches;
+  DynTypedMatcher MaybeBoundMatcher = *Matcher;
+  if (Query.BindRoot) {
+    std::optional<DynTypedMatcher> M = Matcher->tryBind("root");
+    if (M)
+      MaybeBoundMatcher = *M;
+  }
+  CollectBoundNodes Collect(Matches);
+  if (!Finder.addDynamicMatcher(MaybeBoundMatcher, &Collect)) {
+    log("Not a valid top-level matcher.\n");
+    return {/* TODO */};
+  }
+
+  ASTContext &Ctx = AST.getASTContext();
+  Ctx.getParentMapContext().setTraversalKind(Query.Tk);
+  Finder.matchAST(Ctx);
+
+  return Matches;
+}
+
 std::vector<LocatedSymbol> locateSymbolAt(ParsedAST &AST, Position Pos,
                                           const SymbolIndex *Index) {
   const auto &SM = AST.getSourceManager();
diff --git a/clang-tools-extra/clangd/XRefs.h b/clang-tools-extra/clangd/XRefs.h
index 247e52314c3f9..2ece81a8df94a 100644
--- a/clang-tools-extra/clangd/XRefs.h
+++ b/clang-tools-extra/clangd/XRefs.h
@@ -19,6 +19,7 @@
 #include "index/SymbolID.h"
 #include "support/Path.h"
 #include "clang/AST/ASTTypeTraits.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/raw_ostream.h"
 #include <optional>
@@ -32,6 +33,15 @@ class TokenBuffer;
 namespace clangd {
 class ParsedAST;
 
+struct LocatedAST {
+  ast_matchers::BoundNodes &AST;
+};
+
+llvm::raw_ostream &operator<<(llvm::raw_ostream &, const LocatedAST &);
+
+auto locateASTQuery(ParsedAST &AST, SearchASTArgs const &)
+    -> std::vector<ast_matchers::BoundNodes>;
+
 // Describes where a symbol is declared and defined (as far as clangd knows).
 // There are three cases:
 //  - a declaration only, no definition is known (e.g. only header seen)

>From ce1f6de94deea272aba76ec27059b401dca2fccf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fabian=20Ke=C3=9Fler?= <fabian_kessler at gmx.de>
Date: Fri, 29 Aug 2025 20:36:31 +0200
Subject: [PATCH 2/5] WIP: (squash) Improved error handling Cleanup Danger,
 this commit will be sqashed, do not rely on it

---
 clang-tools-extra/clangd/ClangdLSPServer.cpp |  9 +++---
 clang-tools-extra/clangd/ClangdLSPServer.h   |  2 +-
 clang-tools-extra/clangd/ClangdServer.cpp    |  8 +++--
 clang-tools-extra/clangd/XRefs.cpp           | 34 +++++++++-----------
 clang-tools-extra/clangd/XRefs.h             |  2 +-
 5 files changed, 27 insertions(+), 28 deletions(-)

diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index 1330028d160f6..2b1a44b7e8fad 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -76,7 +76,7 @@ std::optional<int64_t> decodeVersion(llvm::StringRef Encoded) {
 const llvm::StringLiteral ApplyFixCommand = "clangd.applyFix";
 const llvm::StringLiteral ApplyTweakCommand = "clangd.applyTweak";
 const llvm::StringLiteral ApplyRenameCommand = "clangd.applyRename";
-constexpr llvm::StringLiteral SearchASTCommand = "clangd.searchAST";
+constexpr llvm::StringLiteral SearchASTMethod = "textDocument/searchAST";
 
 CodeAction toCodeAction(const ClangdServer::CodeActionResult::Rename &R,
                         const URIForFile &File) {
@@ -853,14 +853,13 @@ void ClangdLSPServer::onCommandApplyRename(const RenameParams &R,
   });
 }
 
-void ClangdLSPServer::onCommandSearchAST(const SearchASTArgs &Args,
-                                         Callback<llvm::json::Value> Reply) {
+void ClangdLSPServer::onMethodSearchAST(const SearchASTArgs &Args,
+                                        Callback<llvm::json::Value> Reply) {
   Server->findAST(Args, [Reply = std::move(Reply)](
                             llvm::Expected<std::vector<std::vector<ASTNode>>>
                                 BoundNodes) mutable {
     if (!BoundNodes)
       return Reply(BoundNodes.takeError());
-    auto v = llvm::json::Value(*BoundNodes);
     return Reply(*BoundNodes);
   });
 }
@@ -1741,7 +1740,7 @@ void ClangdLSPServer::bindMethods(LSPBinder &Bind,
   Bind.command(ApplyFixCommand, this, &ClangdLSPServer::onCommandApplyEdit);
   Bind.command(ApplyTweakCommand, this, &ClangdLSPServer::onCommandApplyTweak);
   Bind.command(ApplyRenameCommand, this, &ClangdLSPServer::onCommandApplyRename);
-  Bind.command(SearchASTCommand, this, &ClangdLSPServer::onCommandSearchAST);
+  Bind.method(SearchASTMethod, this, &ClangdLSPServer::onMethodSearchAST);
 
   ApplyWorkspaceEdit = Bind.outgoingMethod("workspace/applyEdit");
   PublishDiagnostics = Bind.outgoingNotification("textDocument/publishDiagnostics");
diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h
index 0daf3a4cc5220..8d7f4ccd67eea 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.h
+++ b/clang-tools-extra/clangd/ClangdLSPServer.h
@@ -186,7 +186,7 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
   void onCommandApplyEdit(const WorkspaceEdit &, Callback<llvm::json::Value>);
   void onCommandApplyTweak(const TweakArgs &, Callback<llvm::json::Value>);
   void onCommandApplyRename(const RenameParams &, Callback<llvm::json::Value>);
-  void onCommandSearchAST(const SearchASTArgs &, Callback<llvm::json::Value>);
+  void onMethodSearchAST(const SearchASTArgs &, Callback<llvm::json::Value>);
 
   /// Outgoing LSP calls.
   LSPBinder::OutgoingMethod<ApplyWorkspaceEditParams,
diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp
index 3d5b75f4511e4..3227b6d8c3fd9 100644
--- a/clang-tools-extra/clangd/ClangdServer.cpp
+++ b/clang-tools-extra/clangd/ClangdServer.cpp
@@ -817,14 +817,16 @@ void ClangdServer::findAST(SearchASTArgs const &Args,
         if (!InpAST)
           return CB(InpAST.takeError());
         auto BoundNodes = clangd::locateASTQuery(InpAST->AST, Args);
-        if (BoundNodes.empty())
+        if (!BoundNodes)
+          return CB(BoundNodes.takeError());
+        if (BoundNodes->empty())
           return CB(error("No matching AST nodes found"));
 
         auto &&AST = InpAST->AST;
         // Convert BoundNodes to a vector of vectors to ASTNode's.
         std::vector<std::vector<ASTNode>> Result;
-        Result.reserve(BoundNodes.size());
-        for (auto &&BN : BoundNodes) {
+        Result.reserve(BoundNodes->size());
+        for (auto &&BN : *BoundNodes) {
           auto &&Map = BN.getMap();
           std::vector<ASTNode> Nodes;
           Nodes.reserve(Map.size());
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index fcfa7ce42527e..0c0c093398d97 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -73,6 +73,7 @@
 #include "llvm/Support/ErrorHandling.h"
 #include "llvm/Support/Path.h"
 #include "llvm/Support/raw_ostream.h"
+#include <algorithm>
 #include <cmath>
 #include <optional>
 #include <string>
@@ -782,57 +783,54 @@ const syntax::Token *findNearbyIdentifier(const SpelledWord &Word,
 }
 
 auto locateASTQuery(ParsedAST &AST, SearchASTArgs const &Query)
-    -> std::vector<ast_matchers::BoundNodes> {
+    -> llvm::Expected<std::vector<ast_matchers::BoundNodes>> {
   using namespace ast_matchers;
   using namespace ast_matchers::dynamic;
   using ast_matchers::dynamic::Parser;
 
   Diagnostics Diag;
   auto MatcherSource = llvm::StringRef(Query.searchQuery).ltrim();
-  auto OrigMatcherSource = MatcherSource;
 
   std::optional<DynTypedMatcher> Matcher = Parser::parseMatcherExpression(
       MatcherSource,
       nullptr /* is this sema instance usefull, to reduce overhead?*/,
       nullptr /*we currently don't support let*/, &Diag);
   if (!Matcher) {
-    elog("Not a valid top-level matcher.\n");
-    return {/* TODO */};
+    return error("Not a valid top-level matcher: {}.", Diag.toString());
   }
-  auto ActualSource = OrigMatcherSource.slice(0, OrigMatcherSource.size() -
-                                                     MatcherSource.size());
-  auto *Q = new query::MatchQuery(ActualSource, *Matcher);
-  Q->RemainingContent = MatcherSource;
 
-  // Q->run(AST);:
-  //==
 
   struct CollectBoundNodes : MatchFinder::MatchCallback {
-    std::vector<BoundNodes> &Bindings;
-    CollectBoundNodes(std::vector<BoundNodes> &Bindings) : Bindings(Bindings) {}
+    std::vector<BoundNodes> *Bindings;
+    CollectBoundNodes(std::vector<BoundNodes> &Bindings)
+        : Bindings(&Bindings) {}
     void run(const MatchFinder::MatchResult &Result) override {
-      Bindings.push_back(Result.Nodes);
+      Bindings->push_back(Result.Nodes);
     }
   };
 
-  MatchFinder Finder;
-  std::vector<BoundNodes> Matches;
   DynTypedMatcher MaybeBoundMatcher = *Matcher;
   if (Query.BindRoot) {
     std::optional<DynTypedMatcher> M = Matcher->tryBind("root");
     if (M)
       MaybeBoundMatcher = *M;
   }
+  std::vector<BoundNodes> Matches;
   CollectBoundNodes Collect(Matches);
+
+  MatchFinder::MatchFinderOptions Opt;
+  Opt.IgnoreSystemHeaders = true;
+  MatchFinder Finder{Opt};
   if (!Finder.addDynamicMatcher(MaybeBoundMatcher, &Collect)) {
-    log("Not a valid top-level matcher.\n");
-    return {/* TODO */};
+    return error("Can't add matcher.");
   }
 
   ASTContext &Ctx = AST.getASTContext();
+
+  auto OldTK = Ctx.getParentMapContext().getTraversalKind();
   Ctx.getParentMapContext().setTraversalKind(Query.Tk);
   Finder.matchAST(Ctx);
-
+  Ctx.getParentMapContext().setTraversalKind(OldTK);
   return Matches;
 }
 
diff --git a/clang-tools-extra/clangd/XRefs.h b/clang-tools-extra/clangd/XRefs.h
index 2ece81a8df94a..d6c0e3b99941b 100644
--- a/clang-tools-extra/clangd/XRefs.h
+++ b/clang-tools-extra/clangd/XRefs.h
@@ -40,7 +40,7 @@ struct LocatedAST {
 llvm::raw_ostream &operator<<(llvm::raw_ostream &, const LocatedAST &);
 
 auto locateASTQuery(ParsedAST &AST, SearchASTArgs const &)
-    -> std::vector<ast_matchers::BoundNodes>;
+    -> llvm::Expected<std::vector<ast_matchers::BoundNodes>>;
 
 // Describes where a symbol is declared and defined (as far as clangd knows).
 // There are three cases:

>From 3c84aeee77aa1a996506507577b8cc95c653a038 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fabian=20Ke=C3=9Fler?= <fabian_kessler at gmx.de>
Date: Fri, 29 Aug 2025 21:31:13 +0200
Subject: [PATCH 3/5] Return the bound node names

---
 clang-tools-extra/clangd/ClangdLSPServer.cpp |  2 +-
 clang-tools-extra/clangd/ClangdServer.cpp    | 14 ++++++--------
 clang-tools-extra/clangd/ClangdServer.h      |  3 ++-
 clang-tools-extra/clangd/Protocol.h          |  1 +
 llvm/include/llvm/Support/JSON.h             |  4 ++++
 5 files changed, 14 insertions(+), 10 deletions(-)

diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index 2b1a44b7e8fad..5cdf615edd8cd 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -856,7 +856,7 @@ void ClangdLSPServer::onCommandApplyRename(const RenameParams &R,
 void ClangdLSPServer::onMethodSearchAST(const SearchASTArgs &Args,
                                         Callback<llvm::json::Value> Reply) {
   Server->findAST(Args, [Reply = std::move(Reply)](
-                            llvm::Expected<std::vector<std::vector<ASTNode>>>
+                            llvm::Expected<BoundASTNodes>
                                 BoundNodes) mutable {
     if (!BoundNodes)
       return Reply(BoundNodes.takeError());
diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp
index 3227b6d8c3fd9..03f220af16610 100644
--- a/clang-tools-extra/clangd/ClangdServer.cpp
+++ b/clang-tools-extra/clangd/ClangdServer.cpp
@@ -811,7 +811,7 @@ void ClangdServer::locateSymbolAt(PathRef File, Position Pos,
 }
 
 void ClangdServer::findAST(SearchASTArgs const &Args,
-                           Callback<std::vector<std::vector<ASTNode>>> CB) {
+                           Callback<BoundASTNodes> CB) {
   auto Action =
       [Args, CB = std::move(CB)](llvm::Expected<InputsAndAST> InpAST) mutable {
         if (!InpAST)
@@ -824,19 +824,17 @@ void ClangdServer::findAST(SearchASTArgs const &Args,
 
         auto &&AST = InpAST->AST;
         // Convert BoundNodes to a vector of vectors to ASTNode's.
-        std::vector<std::vector<ASTNode>> Result;
+        BoundASTNodes Result;
         Result.reserve(BoundNodes->size());
         for (auto &&BN : *BoundNodes) {
           auto &&Map = BN.getMap();
-          std::vector<ASTNode> Nodes;
-          Nodes.reserve(Map.size());
+          BoundASTNodes::value_type BAN;
           for (const auto &[Key, Value] : Map) {
-            auto Node = dumpAST(Value, AST.getTokens(), AST.getASTContext());
-            Nodes.push_back(std::move(Node));
+            BAN.emplace(Key, dumpAST(Value, AST.getTokens(), AST.getASTContext()));
           }
-          if (Nodes.empty())
+          if (BAN.empty())
             continue;
-          Result.push_back(std::move(Nodes));
+          Result.push_back(std::move(BAN));
         }
         if (Result.empty()) {
           return CB(error("No AST nodes found for the query"));
diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h
index e2ee463d53b50..7a299f89c3cd6 100644
--- a/clang-tools-extra/clangd/ClangdServer.h
+++ b/clang-tools-extra/clangd/ClangdServer.h
@@ -37,6 +37,7 @@
 #include "llvm/ADT/FunctionExtras.h"
 #include "llvm/ADT/StringRef.h"
 #include <functional>
+#include <map>
 #include <memory>
 #include <optional>
 #include <string>
@@ -263,7 +264,7 @@ class ClangdServer {
                       Callback<std::vector<LocatedSymbol>> CB);
 
   void findAST(const SearchASTArgs &Args,
-               Callback<std::vector<std::vector<ASTNode>>> CB);
+               Callback<BoundASTNodes> CB);
 
   /// Switch to a corresponding source file when given a header file, and vice
   /// versa.
diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h
index 1d854f19a5eee..1a1864cc1e90a 100644
--- a/clang-tools-extra/clangd/Protocol.h
+++ b/clang-tools-extra/clangd/Protocol.h
@@ -1463,6 +1463,7 @@ struct SearchASTArgs {
   static auto constexpr Tk = TraversalKind::TK_IgnoreUnlessSpelledInSource;
 };
 bool fromJSON(const llvm::json::Value &, SearchASTArgs &, llvm::json::Path);
+using BoundASTNodes = std::vector<std::map<std::string, struct ASTNode>>;
 
 struct PrepareRenameResult {
   /// Range of the string to rename.
diff --git a/llvm/include/llvm/Support/JSON.h b/llvm/include/llvm/Support/JSON.h
index 74858ec559932..05338f7966e0d 100644
--- a/llvm/include/llvm/Support/JSON.h
+++ b/llvm/include/llvm/Support/JSON.h
@@ -111,6 +111,10 @@ class Object {
   // (using std::pair forces extra copies).
   struct KV;
   explicit Object(std::initializer_list<KV> Properties);
+  template <typename Collection> explicit Object(Collection &&C) {
+    for (auto &&P : C)
+      M.insert(P);
+  }
 
   iterator begin() { return M.begin(); }
   const_iterator begin() const { return M.begin(); }

>From 288bf22a0e8ca9cd5a0772dea1bec45443e41f6e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fabian=20Ke=C3=9Fler?= <fabian_kessler at gmx.de>
Date: Sat, 30 Aug 2025 20:00:05 +0200
Subject: [PATCH 4/5] format

---
 clang-tools-extra/clangd/ClangdLSPServer.cpp |  3 +-
 clang-tools-extra/clangd/ClangdServer.cpp    | 58 ++++++++++----------
 clang-tools-extra/clangd/ClangdServer.h      |  3 +-
 clang-tools-extra/clangd/Protocol.cpp        | 15 +++--
 clang-tools-extra/clangd/XRefs.cpp           |  1 -
 5 files changed, 40 insertions(+), 40 deletions(-)

diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index 5cdf615edd8cd..e064b03fea8ae 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -856,8 +856,7 @@ void ClangdLSPServer::onCommandApplyRename(const RenameParams &R,
 void ClangdLSPServer::onMethodSearchAST(const SearchASTArgs &Args,
                                         Callback<llvm::json::Value> Reply) {
   Server->findAST(Args, [Reply = std::move(Reply)](
-                            llvm::Expected<BoundASTNodes>
-                                BoundNodes) mutable {
+                            llvm::Expected<BoundASTNodes> BoundNodes) mutable {
     if (!BoundNodes)
       return Reply(BoundNodes.takeError());
     return Reply(*BoundNodes);
diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp
index 03f220af16610..e2473dd7d3084 100644
--- a/clang-tools-extra/clangd/ClangdServer.cpp
+++ b/clang-tools-extra/clangd/ClangdServer.cpp
@@ -812,35 +812,35 @@ void ClangdServer::locateSymbolAt(PathRef File, Position Pos,
 
 void ClangdServer::findAST(SearchASTArgs const &Args,
                            Callback<BoundASTNodes> CB) {
-  auto Action =
-      [Args, CB = std::move(CB)](llvm::Expected<InputsAndAST> InpAST) mutable {
-        if (!InpAST)
-          return CB(InpAST.takeError());
-        auto BoundNodes = clangd::locateASTQuery(InpAST->AST, Args);
-        if (!BoundNodes)
-          return CB(BoundNodes.takeError());
-        if (BoundNodes->empty())
-          return CB(error("No matching AST nodes found"));
-
-        auto &&AST = InpAST->AST;
-        // Convert BoundNodes to a vector of vectors to ASTNode's.
-        BoundASTNodes Result;
-        Result.reserve(BoundNodes->size());
-        for (auto &&BN : *BoundNodes) {
-          auto &&Map = BN.getMap();
-          BoundASTNodes::value_type BAN;
-          for (const auto &[Key, Value] : Map) {
-            BAN.emplace(Key, dumpAST(Value, AST.getTokens(), AST.getASTContext()));
-          }
-          if (BAN.empty())
-            continue;
-          Result.push_back(std::move(BAN));
-        }
-        if (Result.empty()) {
-          return CB(error("No AST nodes found for the query"));
-        }
-        CB(std::move(Result));
-      };
+  auto Action = [Args, CB = std::move(CB)](
+                    llvm::Expected<InputsAndAST> InpAST) mutable {
+    if (!InpAST)
+      return CB(InpAST.takeError());
+    auto BoundNodes = clangd::locateASTQuery(InpAST->AST, Args);
+    if (!BoundNodes)
+      return CB(BoundNodes.takeError());
+    if (BoundNodes->empty())
+      return CB(error("No matching AST nodes found"));
+
+    auto &&AST = InpAST->AST;
+    // Convert BoundNodes to a vector of vectors to ASTNode's.
+    BoundASTNodes Result;
+    Result.reserve(BoundNodes->size());
+    for (auto &&BN : *BoundNodes) {
+      auto &&Map = BN.getMap();
+      BoundASTNodes::value_type BAN;
+      for (const auto &[Key, Value] : Map) {
+        BAN.emplace(Key, dumpAST(Value, AST.getTokens(), AST.getASTContext()));
+      }
+      if (BAN.empty())
+        continue;
+      Result.push_back(std::move(BAN));
+    }
+    if (Result.empty()) {
+      return CB(error("No AST nodes found for the query"));
+    }
+    CB(std::move(Result));
+  };
 
   WorkScheduler->runWithAST("Definitions", Args.textDocument.uri.file(),
                             std::move(Action));
diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h
index 7a299f89c3cd6..0fd3f15b93674 100644
--- a/clang-tools-extra/clangd/ClangdServer.h
+++ b/clang-tools-extra/clangd/ClangdServer.h
@@ -263,8 +263,7 @@ class ClangdServer {
   void locateSymbolAt(PathRef File, Position Pos,
                       Callback<std::vector<LocatedSymbol>> CB);
 
-  void findAST(const SearchASTArgs &Args,
-               Callback<BoundASTNodes> CB);
+  void findAST(const SearchASTArgs &Args, Callback<BoundASTNodes> CB);
 
   /// Switch to a corresponding source file when given a header file, and vice
   /// versa.
diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp
index 5012dc7edf584..ff946298b0c2a 100644
--- a/clang-tools-extra/clangd/Protocol.cpp
+++ b/clang-tools-extra/clangd/Protocol.cpp
@@ -1651,13 +1651,16 @@ bool fromJSON(const llvm::json::Value &Params, SelectionRangeParams &S,
          O.map("positions", S.positions);
 }
 
-bool fromJSON(const llvm::json::Value &Params, SearchASTArgs &Args, llvm::json::Path P) {
+bool fromJSON(const llvm::json::Value &Params, SearchASTArgs &Args,
+              llvm::json::Path P) {
   llvm::json::ObjectMapper O(Params, P);
-  return O && O.map("query", Args.searchQuery)
-  && O.map("textDocument", Args.textDocument)
-  // && O.map("bindRoot", Args.bindRoot); TODO: add bindRoot to extend this feature
-  // && O.map("traversalKind", Args.tk); TODO: add traversalKind to extend this feature
-  ;
+  return O && O.map("query", Args.searchQuery) &&
+         O.map("textDocument", Args.textDocument)
+      // && O.map("bindRoot", Args.bindRoot); TODO: add bindRoot to extend this
+      // feature
+      // && O.map("traversalKind", Args.tk); TODO: add traversalKind to extend
+      // this feature
+      ;
 }
 
 llvm::json::Value toJSON(const SelectionRange &Out) {
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index 0c0c093398d97..ea9da2adf8287 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -799,7 +799,6 @@ auto locateASTQuery(ParsedAST &AST, SearchASTArgs const &Query)
     return error("Not a valid top-level matcher: {}.", Diag.toString());
   }
 
-
   struct CollectBoundNodes : MatchFinder::MatchCallback {
     std::vector<BoundNodes> *Bindings;
     CollectBoundNodes(std::vector<BoundNodes> &Bindings)

>From de0fa1048ef37b03103dbe89b8b751e3e6b1c7c6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fabian=20Ke=C3=9Fler?= <fabian_kessler at gmx.de>
Date: Thu, 4 Sep 2025 18:52:56 +0200
Subject: [PATCH 5/5] Add test, add astSearchProvider to capabilities

---
 clang-tools-extra/clangd/ClangdLSPServer.cpp  |  1 +
 .../clangd/test/find-in-ast.test              | 37 +++++++++++++++++++
 2 files changed, 38 insertions(+)
 create mode 100644 clang-tools-extra/clangd/test/find-in-ast.test

diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index e064b03fea8ae..d7f1f683dd71b 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -639,6 +639,7 @@ void ClangdLSPServer::onInitialize(const InitializeParams &Params,
       {"workspaceSymbolProvider", true},
       {"referencesProvider", true},
       {"astProvider", true}, // clangd extension
+      {"astSearchProvider", llvm::json::Object{{"search", true},{"replace", false}}}, // clangd extension
       {"typeHierarchyProvider", true},
       // Unfortunately our extension made use of the same capability name as the
       // standard. Advertise this capability to tell clients that implement our
diff --git a/clang-tools-extra/clangd/test/find-in-ast.test b/clang-tools-extra/clangd/test/find-in-ast.test
new file mode 100644
index 0000000000000..6031ad10de1b3
--- /dev/null
+++ b/clang-tools-extra/clangd/test/find-in-ast.test
@@ -0,0 +1,37 @@
+# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace --dump-input always %s
+void bob();
+void f() {
+  bob();
+}
+---
+{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{"textDocument": {"foldingRange": {"lineFoldingOnly": true}}},"trace":"off"}}
+---
+{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"languageId":"cpp","text":"void bob();\nvoid f() {\n  bob();\n}\n","uri":"test:///foo.cpp","version":1}}}
+---
+{"id":1,"jsonrpc":"2.0","method":"textDocument/searchAST","params":{"textDocument":{"uri":"test:///foo.cpp"},"query":"declRefExpr(to(namedDecl(hasName(\"bob\"))))"}}
+#      CHECK:  "id": 1,
+# CHECK-NEXT:  "jsonrpc": "2.0",
+# CHECK-NEXT:  "result": [
+# CHECK-NEXT:    {
+# CHECK-NEXT:      "root": {
+# CHECK-NEXT:        "arcana": "DeclRefExpr {{.*}} 'void ()' lvalue Function {{.*}} 'bob' 'void ()'",
+# CHECK-NEXT:        "detail": "bob",
+# CHECK-NEXT:        "kind": "DeclRef",
+# CHECK-NEXT:        "range": {
+# CHECK-NEXT:          "end": {
+# CHECK-NEXT:            "character": 5,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          },
+# CHECK-NEXT:          "start": {
+# CHECK-NEXT:            "character": 2,
+# CHECK-NEXT:            "line": 2
+# CHECK-NEXT:          }
+# CHECK-NEXT:        },
+# CHECK-NEXT:        "role": "expression"
+# CHECK-NEXT:      }
+# CHECK-NEXT:    }
+# CHECK-NEXT:  ]
+---
+{"jsonrpc":"2.0","id":5,"method":"shutdown"}
+---
+{"jsonrpc":"2.0","method":"exit"}



More information about the llvm-commits mailing list