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
