https://github.com/kastiglione updated 
https://github.com/llvm/llvm-project/pull/203383

>From 5eb99ecf665fcdc8910c19f27c965f27b2609e25 Mon Sep 17 00:00:00 2001
From: Dave Lee <[email protected]>
Date: Thu, 11 Jun 2026 13:18:17 -0700
Subject: [PATCH 1/3] [clang-query] Add JSON output mode for machine-readable
 results

Add `set output json` which emits match results as compact JSON using clang's 
existing
`JSONDumper`. This enables scripted, and may also be useful for AI-agent 
consumption.

The JSON output includes the matcher expression, match count, and for each 
binding: the
node kind, source range, and full AST detail (same format as `clang 
-ast-dump=json`).

Adds an `IndentSize` parameter to `JSONDumper`/`NodeStreamer` (defaulting to 2) 
so
callers can request compact output.

Also refactors `MatchQuery::run` to collect matches in a single pass before 
branching on
output format.

Assisted-by: claude
---
 clang-tools-extra/clang-query/Query.cpp       | 110 +++++++++++++++++-
 clang-tools-extra/clang-query/Query.h         |   3 +-
 clang-tools-extra/clang-query/QueryParser.cpp |   7 +-
 clang-tools-extra/clang-query/QuerySession.h  |   6 +-
 .../test/clang-query/json-output.c            |   9 ++
 clang/include/clang/AST/JSONNodeDumper.h      |  11 +-
 6 files changed, 131 insertions(+), 15 deletions(-)
 create mode 100644 clang-tools-extra/test/clang-query/json-output.c

diff --git a/clang-tools-extra/clang-query/Query.cpp 
b/clang-tools-extra/clang-query/Query.cpp
index 574b64ee0f759..afd1863055343 100644
--- a/clang-tools-extra/clang-query/Query.cpp
+++ b/clang-tools-extra/clang-query/Query.cpp
@@ -10,11 +10,15 @@
 #include "QueryParser.h"
 #include "QuerySession.h"
 #include "clang/AST/ASTDumper.h"
+#include "clang/AST/JSONNodeDumper.h"
 #include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
 #include "clang/Frontend/ASTUnit.h"
 #include "clang/Frontend/TextDiagnostic.h"
+#include "llvm/Support/JSON.h"
 #include "llvm/Support/raw_ostream.h"
 #include <optional>
+#include <vector>
 
 using namespace clang::ast_matchers;
 using namespace clang::ast_matchers::dynamic;
@@ -71,7 +75,9 @@ bool HelpQuery::run(llvm::raw_ostream &OS, QuerySession &QS) 
const {
         "  detailed-ast                      "
         "Detailed AST output for bound nodes.\n"
         "  dump                              "
-        "Detailed AST output for bound nodes (alias of detailed-ast).\n\n";
+        "Detailed AST output for bound nodes (alias of detailed-ast).\n"
+        "  json                              "
+        "JSON output for bound nodes (structured, machine-readable).\n\n";
   return true;
 }
 
@@ -107,12 +113,14 @@ struct QueryProfiler {
 } // namespace
 
 bool MatchQuery::run(llvm::raw_ostream &OS, QuerySession &QS) const {
-  unsigned MatchCount = 0;
-
   std::optional<QueryProfiler> Profiler;
   if (QS.EnableProfile)
     Profiler.emplace();
 
+  // Collect matches from all ASTs.
+  using ASTMatchResult = std::pair<ASTUnit *, std::vector<BoundNodes>>;
+  SmallVector<ASTMatchResult> AllMatches;
+
   for (auto &AST : QS.ASTs) {
     ast_matchers::MatchFinder::MatchFinderOptions FinderOptions;
     std::optional<llvm::StringMap<llvm::TimeRecord>> Records;
@@ -142,6 +150,97 @@ bool MatchQuery::run(llvm::raw_ostream &OS, QuerySession 
&QS) const {
     if (QS.EnableProfile)
       Profiler->Records[OrigSrcName] += (*Records)[OrigSrcName];
 
+    AllMatches.emplace_back(AST.get(), std::move(Matches));
+  }
+
+  unsigned MatchCount = 0;
+  for (const auto &[_, Matches] : AllMatches)
+    MatchCount += Matches.size();
+
+  if (QS.JSONOutput) {
+    llvm::json::OStream JOS(OS);
+
+    // RAII helpers that pair begin/end calls so JSON nesting mirrors C++ 
scope.
+    struct ObjectScope {
+      llvm::json::OStream &J;
+      ObjectScope(llvm::json::OStream &J) : J(J) { J.objectBegin(); }
+      ~ObjectScope() { J.objectEnd(); }
+    };
+    struct AttributeScope {
+      llvm::json::OStream &J;
+      AttributeScope(llvm::json::OStream &J, StringRef Key) : J(J) {
+        J.attributeBegin(Key);
+      }
+      ~AttributeScope() { J.attributeEnd(); }
+    };
+    struct ArrayScope {
+      llvm::json::OStream &J;
+      ArrayScope(llvm::json::OStream &J) : J(J) { J.arrayBegin(); }
+      ~ArrayScope() { J.arrayEnd(); }
+    };
+
+    {
+      ObjectScope Root(JOS);
+      JOS.attribute("matcher", Source);
+      JOS.attribute("match_count", MatchCount);
+      AttributeScope MatchesAttr(JOS, "matches");
+      ArrayScope MatchesArr(JOS);
+
+      for (auto &[AST, Matches] : AllMatches) {
+        for (const auto &Match : Matches) {
+          ObjectScope MatchObj(JOS);
+          AttributeScope BindingsAttr(JOS, "bindings");
+          ObjectScope BindingsObj(JOS);
+
+          for (const auto &[Name, Node] : Match.getMap()) {
+            AttributeScope BindingAttr(JOS, Name);
+            ObjectScope BindingObj(JOS);
+
+            JOS.attribute("kind", Node.getNodeKind().asStringRef());
+
+            SourceRange R = Node.getSourceRange();
+            if (R.isValid()) {
+              SourceManager &SM = AST->getSourceManager();
+              FullSourceLoc Begin(R.getBegin(), SM);
+              FullSourceLoc End(R.getEnd(), SM);
+              AttributeScope RangeAttr(JOS, "range");
+              ObjectScope RangeObj(JOS);
+              JOS.attribute("file", SM.getFilename(R.getBegin()));
+              {
+                AttributeScope BeginAttr(JOS, "begin");
+                ObjectScope BeginObj(JOS);
+                JOS.attribute("line", Begin.getSpellingLineNumber());
+                JOS.attribute("col", Begin.getSpellingColumnNumber());
+              }
+              {
+                AttributeScope EndAttr(JOS, "end");
+                ObjectScope EndObj(JOS);
+                JOS.attribute("line", End.getSpellingLineNumber());
+                JOS.attribute("col", End.getSpellingColumnNumber());
+              }
+            }
+
+            AttributeScope DetailAttr(JOS, "detail");
+            std::string DetailStr;
+            llvm::raw_string_ostream DetailOS(DetailStr);
+            ASTContext &Ctx = AST->getASTContext();
+            JSONDumper Dumper(DetailOS, AST->getSourceManager(), Ctx,
+                             Ctx.getPrintingPolicy(),
+                             &Ctx.getCommentCommandTraits(),
+                             /*IndentSize=*/0);
+            Dumper.SetTraversalKind(QS.TK);
+            Dumper.Visit(Node);
+            JOS.rawValue(DetailStr);
+          }
+        }
+      }
+    }
+    OS << "\n";
+    return true;
+  }
+
+  unsigned MatchIdx = 0;
+  for (const auto &[AST, Matches] : AllMatches) {
     if (QS.PrintMatcher) {
       SmallVector<StringRef, 4> Lines;
       Source.split(Lines, "\n");
@@ -164,7 +263,7 @@ bool MatchQuery::run(llvm::raw_ostream &OS, QuerySession 
&QS) const {
     }
 
     for (auto MI = Matches.begin(), ME = Matches.end(); MI != ME; ++MI) {
-      OS << "\nMatch #" << ++MatchCount << ":\n\n";
+      OS << "\nMatch #" << ++MatchIdx << ":\n\n";
 
       for (auto BI = MI->getMap().begin(), BE = MI->getMap().end(); BI != BE;
            ++BI) {
@@ -186,7 +285,8 @@ bool MatchQuery::run(llvm::raw_ostream &OS, QuerySession 
&QS) const {
         }
         if (QS.DetailedASTOutput) {
           OS << "Binding for \"" << BI->first << "\":\n";
-          ASTDumper Dumper(OS, Ctx, AST->getDiagnostics().getShowColors());
+          ASTDumper Dumper(OS, AST->getASTContext(),
+                           AST->getDiagnostics().getShowColors());
           Dumper.SetTraversalKind(QS.TK);
           Dumper.Visit(BI->second);
           OS << "\n";
diff --git a/clang-tools-extra/clang-query/Query.h 
b/clang-tools-extra/clang-query/Query.h
index af250fbe13ce3..27fc99176fc3d 100644
--- a/clang-tools-extra/clang-query/Query.h
+++ b/clang-tools-extra/clang-query/Query.h
@@ -17,7 +17,7 @@
 namespace clang {
 namespace query {
 
-enum OutputKind { OK_Diag, OK_Print, OK_DetailedAST };
+enum OutputKind { OK_Diag, OK_Print, OK_DetailedAST, OK_JSON };
 
 enum QueryKind {
   QK_Invalid,
@@ -149,6 +149,7 @@ struct SetExclusiveOutputQuery : Query {
     QS.DiagOutput = false;
     QS.DetailedASTOutput = false;
     QS.PrintOutput = false;
+    QS.JSONOutput = false;
     QS.*Var = true;
     return true;
   }
diff --git a/clang-tools-extra/clang-query/QueryParser.cpp 
b/clang-tools-extra/clang-query/QueryParser.cpp
index 1d5ec281defd4..fb0beeb2d4b44 100644
--- a/clang-tools-extra/clang-query/QueryParser.cpp
+++ b/clang-tools-extra/clang-query/QueryParser.cpp
@@ -108,10 +108,11 @@ template <typename QueryType> QueryRef 
QueryParser::parseSetOutputKind() {
                          .Case("print", OK_Print)
                          .Case("detailed-ast", OK_DetailedAST)
                          .Case("dump", OK_DetailedAST)
+                         .Case("json", OK_JSON)
                          .Default(~0u);
   if (OutKind == ~0u) {
-    return new InvalidQuery("expected 'diag', 'print', 'detailed-ast' or "
-                            "'dump', got '" +
+    return new InvalidQuery("expected 'diag', 'print', 'detailed-ast', 'dump' "
+                            "or 'json', got '" +
                             ValStr + "'");
   }
 
@@ -122,6 +123,8 @@ template <typename QueryType> QueryRef 
QueryParser::parseSetOutputKind() {
     return new QueryType(&QuerySession::DiagOutput);
   case OK_Print:
     return new QueryType(&QuerySession::PrintOutput);
+  case OK_JSON:
+    return new QueryType(&QuerySession::JSONOutput);
   }
 
   llvm_unreachable("Invalid output kind");
diff --git a/clang-tools-extra/clang-query/QuerySession.h 
b/clang-tools-extra/clang-query/QuerySession.h
index c7d5a64c33200..2d52a5293528e 100644
--- a/clang-tools-extra/clang-query/QuerySession.h
+++ b/clang-tools-extra/clang-query/QuerySession.h
@@ -25,14 +25,16 @@ class QuerySession {
 public:
   QuerySession(llvm::ArrayRef<std::unique_ptr<ASTUnit>> ASTs)
       : ASTs(ASTs), PrintOutput(false), DiagOutput(true),
-        DetailedASTOutput(false), BindRoot(true), PrintMatcher(false),
-        EnableProfile(false), Terminate(false), TK(TK_AsIs) {}
+        DetailedASTOutput(false), JSONOutput(false), BindRoot(true),
+        PrintMatcher(false), EnableProfile(false), Terminate(false),
+        TK(TK_AsIs) {}
 
   llvm::ArrayRef<std::unique_ptr<ASTUnit>> ASTs;
 
   bool PrintOutput;
   bool DiagOutput;
   bool DetailedASTOutput;
+  bool JSONOutput;
 
   bool BindRoot;
   bool PrintMatcher;
diff --git a/clang-tools-extra/test/clang-query/json-output.c 
b/clang-tools-extra/test/clang-query/json-output.c
new file mode 100644
index 0000000000000..1bcc83a21eaf3
--- /dev/null
+++ b/clang-tools-extra/test/clang-query/json-output.c
@@ -0,0 +1,9 @@
+// RUN: clang-query -c "set output json" -c "match functionDecl()" %s -- | 
FileCheck %s
+
+// CHECK: 
{"matcher":"functionDecl()","match_count":1,"matches":[{"bindings":{"root":{"kind":"FunctionDecl"
+// CHECK-SAME: "range":{"file":"{{.*}}json-output.c","begin":{"line":
+// CHECK-SAME: "detail":{
+// CHECK-SAME: "kind":"FunctionDecl"
+// CHECK-SAME: "name":"foo"
+// CHECK-SAME: "qualType":"void (void)"
+void foo(void) {}
diff --git a/clang/include/clang/AST/JSONNodeDumper.h 
b/clang/include/clang/AST/JSONNodeDumper.h
index 4e8d1649bbf8b..322ed5a37ef78 100644
--- a/clang/include/clang/AST/JSONNodeDumper.h
+++ b/clang/include/clang/AST/JSONNodeDumper.h
@@ -106,7 +106,8 @@ class NodeStreamer {
     FirstChild = false;
   }
 
-  NodeStreamer(raw_ostream &OS) : JOS(OS, 2) {}
+  NodeStreamer(raw_ostream &OS, unsigned IndentSize = 2)
+      : JOS(OS, IndentSize) {}
 };
 
 // Dumps AST nodes in JSON format. There is no implied stability for the
@@ -188,8 +189,8 @@ class JSONNodeDumper
 public:
   JSONNodeDumper(raw_ostream &OS, const SourceManager &SrcMgr, ASTContext &Ctx,
                  const PrintingPolicy &PrintPolicy,
-                 const comments::CommandTraits *Traits)
-      : NodeStreamer(OS), SM(SrcMgr), Ctx(Ctx), ASTNameGen(Ctx),
+                 const comments::CommandTraits *Traits, unsigned IndentSize = 
2)
+      : NodeStreamer(OS, IndentSize), SM(SrcMgr), Ctx(Ctx), ASTNameGen(Ctx),
         PrintPolicy(PrintPolicy), Traits(Traits), LastLocLine(0),
         LastLocPresumedLine(0) {}
 
@@ -447,8 +448,8 @@ class JSONDumper : public ASTNodeTraverser<JSONDumper, 
JSONNodeDumper> {
 public:
   JSONDumper(raw_ostream &OS, const SourceManager &SrcMgr, ASTContext &Ctx,
              const PrintingPolicy &PrintPolicy,
-             const comments::CommandTraits *Traits)
-      : NodeDumper(OS, SrcMgr, Ctx, PrintPolicy, Traits) {}
+             const comments::CommandTraits *Traits, unsigned IndentSize = 2)
+      : NodeDumper(OS, SrcMgr, Ctx, PrintPolicy, Traits, IndentSize) {}
 
   JSONNodeDumper &doGetNodeDelegate() { return NodeDumper; }
 

>From 82755bbaa4838af9c1b434c5b6bb68f21c885e4b Mon Sep 17 00:00:00 2001
From: Dave Lee <[email protected]>
Date: Thu, 11 Jun 2026 13:26:10 -0700
Subject: [PATCH 2/3] clang-format

---
 clang-tools-extra/clang-query/Query.cpp | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/clang-tools-extra/clang-query/Query.cpp 
b/clang-tools-extra/clang-query/Query.cpp
index afd1863055343..e06d0987d6af0 100644
--- a/clang-tools-extra/clang-query/Query.cpp
+++ b/clang-tools-extra/clang-query/Query.cpp
@@ -225,9 +225,9 @@ bool MatchQuery::run(llvm::raw_ostream &OS, QuerySession 
&QS) const {
             llvm::raw_string_ostream DetailOS(DetailStr);
             ASTContext &Ctx = AST->getASTContext();
             JSONDumper Dumper(DetailOS, AST->getSourceManager(), Ctx,
-                             Ctx.getPrintingPolicy(),
-                             &Ctx.getCommentCommandTraits(),
-                             /*IndentSize=*/0);
+                              Ctx.getPrintingPolicy(),
+                              &Ctx.getCommentCommandTraits(),
+                              /*IndentSize=*/0);
             Dumper.SetTraversalKind(QS.TK);
             Dumper.Visit(Node);
             JOS.rawValue(DetailStr);

>From 831ba826e7b07b44c9428e4dc88a7f416d6e2762 Mon Sep 17 00:00:00 2001
From: Dave Lee <[email protected]>
Date: Thu, 11 Jun 2026 13:50:20 -0700
Subject: [PATCH 3/3] Update QueryParserTest.cpp

---
 .../unittests/clang-query/QueryParserTest.cpp        | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/clang-tools-extra/unittests/clang-query/QueryParserTest.cpp 
b/clang-tools-extra/unittests/clang-query/QueryParserTest.cpp
index e414587c568b7..39eae66c516ef 100644
--- a/clang-tools-extra/unittests/clang-query/QueryParserTest.cpp
+++ b/clang-tools-extra/unittests/clang-query/QueryParserTest.cpp
@@ -70,7 +70,7 @@ TEST_F(QueryParserTest, Set) {
 
   Q = parse("set output");
   ASSERT_TRUE(isa<InvalidQuery>(Q));
-  EXPECT_EQ("expected 'diag', 'print', 'detailed-ast' or 'dump', got ''",
+  EXPECT_EQ("expected 'diag', 'print', 'detailed-ast', 'dump' or 'json', got 
''",
             cast<InvalidQuery>(Q)->ErrStr);
 
   Q = parse("set bind-root true foo");
@@ -79,7 +79,7 @@ TEST_F(QueryParserTest, Set) {
 
   Q = parse("set output foo");
   ASSERT_TRUE(isa<InvalidQuery>(Q));
-  EXPECT_EQ("expected 'diag', 'print', 'detailed-ast' or 'dump', got 'foo'",
+  EXPECT_EQ("expected 'diag', 'print', 'detailed-ast', 'dump' or 'json', got 
'foo'",
             cast<InvalidQuery>(Q)->ErrStr);
 
   Q = parse("set output dump");
@@ -90,6 +90,10 @@ TEST_F(QueryParserTest, Set) {
   ASSERT_TRUE(isa<SetExclusiveOutputQuery>(Q));
   EXPECT_EQ(&QuerySession::DetailedASTOutput, 
cast<SetExclusiveOutputQuery>(Q)->Var);
 
+  Q = parse("set output json");
+  ASSERT_TRUE(isa<SetExclusiveOutputQuery>(Q));
+  EXPECT_EQ(&QuerySession::JSONOutput, cast<SetExclusiveOutputQuery>(Q)->Var);
+
   Q = parse("enable output detailed-ast");
   ASSERT_TRUE(isa<EnableOutputQuery>(Q));
   EXPECT_EQ(&QuerySession::DetailedASTOutput, cast<EnableOutputQuery>(Q)->Var);
@@ -221,7 +225,7 @@ TEST_F(QueryParserTest, Complete) {
   EXPECT_EQ("output", Comps[0].DisplayText);
 
   Comps = QueryParser::complete("enable output ", 14, QS);
-  ASSERT_EQ(4u, Comps.size());
+  ASSERT_EQ(5u, Comps.size());
 
   EXPECT_EQ("diag ", Comps[0].TypedText);
   EXPECT_EQ("diag", Comps[0].DisplayText);
@@ -231,6 +235,8 @@ TEST_F(QueryParserTest, Complete) {
   EXPECT_EQ("detailed-ast", Comps[2].DisplayText);
   EXPECT_EQ("dump ", Comps[3].TypedText);
   EXPECT_EQ("dump", Comps[3].DisplayText);
+  EXPECT_EQ("json ", Comps[4].TypedText);
+  EXPECT_EQ("json", Comps[4].DisplayText);
 
   Comps = QueryParser::complete("set traversal ", 14, QS);
   ASSERT_EQ(2u, Comps.size());

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to