[clang-tools-extra] 6fa0e02 - [include-cleaner] Add include-cleaner tool, with initial HTML report

Sam McCall via cfe-commits cfe-commits at lists.llvm.org
Tue Oct 18 09:20:58 PDT 2022


Author: Sam McCall
Date: 2022-10-18T18:09:41+02:00
New Revision: 6fa0e026c87e20b962137691a7852fb5e32f9611

URL: https://github.com/llvm/llvm-project/commit/6fa0e026c87e20b962137691a7852fb5e32f9611
DIFF: https://github.com/llvm/llvm-project/commit/6fa0e026c87e20b962137691a7852fb5e32f9611.diff

LOG: [include-cleaner] Add include-cleaner tool, with initial HTML report

The immediate goal is to start producing an HTML report to debug and explain
include-cleaner recommendations.
For now, this includes only the lowest-level piece: a list of the references
found in the source code.

How this fits into future ideas:
 - under refs we can also show the headers providing the symbol, which includes
   match those headers etc
 - we can also annotate the #include lines with which symbols they cover, and
   add whichever includes we're suggesting too
 - the include-cleaner tool will likely have modes where it emits diagnostics
   and/or applies edits, so the HTML report is behind a flag

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

Added: 
    clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h
    clang-tools-extra/include-cleaner/lib/HTMLReport.cpp
    clang-tools-extra/include-cleaner/lib/Record.cpp
    clang-tools-extra/include-cleaner/test/Inputs/bar.h
    clang-tools-extra/include-cleaner/test/Inputs/foo.h
    clang-tools-extra/include-cleaner/test/html.cpp
    clang-tools-extra/include-cleaner/tool/CMakeLists.txt
    clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp
    clang-tools-extra/include-cleaner/unittests/RecordTest.cpp

Modified: 
    clang-tools-extra/include-cleaner/CMakeLists.txt
    clang-tools-extra/include-cleaner/lib/AnalysisInternal.h
    clang-tools-extra/include-cleaner/lib/CMakeLists.txt
    clang-tools-extra/include-cleaner/unittests/CMakeLists.txt
    clang/include/clang/Testing/TestAST.h
    clang/lib/Testing/TestAST.cpp

Removed: 
    


################################################################################
diff  --git a/clang-tools-extra/include-cleaner/CMakeLists.txt b/clang-tools-extra/include-cleaner/CMakeLists.txt
index 0550b02f603b5..dc147f9ca08df 100644
--- a/clang-tools-extra/include-cleaner/CMakeLists.txt
+++ b/clang-tools-extra/include-cleaner/CMakeLists.txt
@@ -1,4 +1,6 @@
+include_directories(include)
 add_subdirectory(lib)
+add_subdirectory(tool)
 if(CLANG_INCLUDE_TESTS)
   add_subdirectory(test)
   add_subdirectory(unittests)

diff  --git a/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h b/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h
new file mode 100644
index 0000000000000..c3d3a98ee3f3f
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h
@@ -0,0 +1,45 @@
+//===--- Record.h - Record compiler events ------------------------- 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Where Analysis.h analyzes AST nodes and recorded preprocessor events, this
+// file defines ways to capture AST and preprocessor information from a parse.
+//
+// These are the simplest way to connect include-cleaner logic to the parser,
+// but other ways are possible (for example clangd records includes separately).
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef CLANG_INCLUDE_CLEANER_RECORD_H
+#define CLANG_INCLUDE_CLEANER_RECORD_H
+
+#include <memory>
+#include <vector>
+
+namespace clang {
+class ASTConsumer;
+class ASTContext;
+class Decl;
+namespace include_cleaner {
+
+// Contains recorded parser events relevant to include-cleaner.
+struct RecordedAST {
+  // The consumer (when installed into clang) tracks declarations in this.
+  std::unique_ptr<ASTConsumer> record();
+
+  ASTContext *Ctx = nullptr;
+  // The set of declarations written at file scope inside the main file.
+  //
+  // These are the roots of the subtrees that should be traversed to find uses.
+  // (Traversing the TranslationUnitDecl would find uses inside headers!)
+  std::vector<Decl *> Roots;
+};
+
+} // namespace include_cleaner
+} // namespace clang
+
+#endif
\ No newline at end of file

diff  --git a/clang-tools-extra/include-cleaner/lib/AnalysisInternal.h b/clang-tools-extra/include-cleaner/lib/AnalysisInternal.h
index 8b0c73fe7997b..63d62be0e218d 100644
--- a/clang-tools-extra/include-cleaner/lib/AnalysisInternal.h
+++ b/clang-tools-extra/include-cleaner/lib/AnalysisInternal.h
@@ -25,6 +25,7 @@
 #include "llvm/ADT/STLFunctionalExtras.h"
 
 namespace clang {
+class ASTContext;
 class Decl;
 class NamedDecl;
 namespace include_cleaner {
@@ -41,6 +42,11 @@ namespace include_cleaner {
 /// being analyzed, in order to find all references within it.
 void walkAST(Decl &Root, llvm::function_ref<void(SourceLocation, NamedDecl &)>);
 
+/// Write an HTML summary of the analysis to the given stream.
+/// FIXME: Once analysis has a public API, this should be public too.
+void writeHTMLReport(FileID File, llvm::ArrayRef<Decl *> Roots, ASTContext &Ctx,
+                     llvm::raw_ostream &OS);
+
 } // namespace include_cleaner
 } // namespace clang
 

diff  --git a/clang-tools-extra/include-cleaner/lib/CMakeLists.txt b/clang-tools-extra/include-cleaner/lib/CMakeLists.txt
index 5e2807332f94d..f65f50c243b86 100644
--- a/clang-tools-extra/include-cleaner/lib/CMakeLists.txt
+++ b/clang-tools-extra/include-cleaner/lib/CMakeLists.txt
@@ -1,6 +1,8 @@
 set(LLVM_LINK_COMPONENTS Support)
 
 add_clang_library(clangIncludeCleaner
+  HTMLReport.cpp
+  Record.cpp
   WalkAST.cpp
 
   LINK_LIBS

diff  --git a/clang-tools-extra/include-cleaner/lib/HTMLReport.cpp b/clang-tools-extra/include-cleaner/lib/HTMLReport.cpp
new file mode 100644
index 0000000000000..c7bfc369776fa
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/lib/HTMLReport.cpp
@@ -0,0 +1,198 @@
+//===--- HTMLReport.cpp - Explain the analysis for humans -----------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// If we're debugging this tool or trying to explain its conclusions, we need to
+// be able to identify specific facts about the code and the inferences made.
+//
+// This library prints an annotated version of the code
+//
+//===----------------------------------------------------------------------===//
+
+#include "AnalysisInternal.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/PrettyPrinter.h"
+#include "clang/Basic/SourceManager.h"
+#include "clang/Lex/Lexer.h"
+#include "llvm/Support/raw_ostream.h"
+
+namespace clang::include_cleaner {
+namespace {
+
+constexpr llvm::StringLiteral CSS = R"css(
+  pre { line-height: 1.5em; }
+  .ref { text-decoration: underline; color: #008; }
+  .sel { position: relative; cursor: pointer; }
+  #hover {
+    background-color: #aaccff; border: 1px solid black;
+    z-index: 1;
+    position: absolute; top: 100%; left: 0;
+    font-family: sans-serif;
+    padding: 0.5em;
+  }
+  #hover p, #hover pre { margin: 0; }
+  #hover section header { font-weight: bold; }
+  #hover section:not(:first-child) { margin-top: 1em; }
+)css";
+
+constexpr llvm::StringLiteral JS = R"js(
+  // Recreate the #hover div inside whichever target .sel element was clicked.
+  function select(event) {
+    var target = event.target.closest('.sel');
+    var hover = document.getElementById('hover');
+    if (hover) {
+      if (hover.parentElement == target) return;
+      hover.parentNode.removeChild(hover);
+    }
+    if (target == null) return;
+    hover = document.createElement('div');
+    hover.id = 'hover';
+    fillHover(hover, target);
+    target.appendChild(hover);
+  }
+  // Fill the #hover div with the templates named by data-hover in the target.
+  function fillHover(hover, target) {
+    target.dataset.hover?.split(',').forEach(function(id) {
+      for (c of document.getElementById(id).content.childNodes)
+        hover.appendChild(c.cloneNode(true));
+    })
+  }
+)js";
+
+// Print the declaration tersely, but enough to identify e.g. which overload.
+std::string printDecl(const NamedDecl &ND) {
+  std::string S;
+  llvm::raw_string_ostream OS(S);
+  PrintingPolicy PP = ND.getASTContext().getPrintingPolicy();
+  PP.FullyQualifiedName = true;
+  PP.TerseOutput = true;
+  PP.SuppressInitializers = true;
+  ND.print(OS, PP);
+  llvm::erase_value(S, '\n');
+  return S;
+}
+
+class Reporter {
+  llvm::raw_ostream &OS;
+  const ASTContext &Ctx;
+  const SourceManager &SM;
+  FileID File;
+
+  struct Target {
+    const NamedDecl *D;
+  };
+  std::vector<Target> Targets;
+  std::vector<std::pair</*Offset*/ unsigned, /*TargetIndex*/ unsigned>> Refs;
+
+public:
+  Reporter(llvm::raw_ostream &OS, ASTContext &Ctx, FileID File)
+      : OS(OS), Ctx(Ctx), SM(Ctx.getSourceManager()), File(File) {}
+
+  void addRef(SourceLocation Loc, const NamedDecl &D) {
+    auto Coords = SM.getDecomposedLoc(SM.getFileLoc(Loc));
+    if (Coords.first != File)
+      llvm::errs() << "Ref location outside file!\n";
+    Targets.push_back({&D});
+    Refs.push_back({Coords.second, Targets.size() - 1});
+  }
+
+  void write() {
+    OS << "<!doctype html>\n";
+    OS << "<html>\n";
+    OS << "<head>\n";
+    OS << "<style>" << CSS << "</style>\n";
+    OS << "<script>" << JS << "</script>\n";
+    for (unsigned I = 0; I < Targets.size(); ++I) {
+      OS << "<template id='t" << I << "'><section>";
+      writeTarget(Targets[I]);
+      OS << "</section></template>\n";
+    }
+    OS << "</head>\n";
+    OS << "<body>\n";
+    writeCode();
+    OS << "</body>\n";
+    OS << "</html>\n";
+  }
+
+private:
+  void escapeChar(char C) {
+    switch (C) {
+    case '<':
+      OS << "<";
+      break;
+    case '&':
+      OS << "&";
+      break;
+    default:
+      OS << C;
+    }
+  }
+
+  void escapeString(llvm::StringRef S) {
+    for (char C : S)
+      escapeChar(C);
+  }
+
+  void writeTarget(const Target &T) {
+    OS << "<header>" << T.D->getDeclKindName() << " ";
+    escapeString(T.D->getQualifiedNameAsString());
+    OS << "</header>";
+
+    OS << "<p>declared at ";
+    escapeString(SM.getFileLoc(T.D->getLocation()).printToString(SM));
+    OS << "</p><pre>";
+    escapeString(printDecl(*T.D));
+    OS << "</pre>";
+  }
+
+  void writeCode() {
+    llvm::sort(Refs);
+    llvm::StringRef Code = SM.getBufferData(File);
+
+    OS << "<pre onclick='select(event)'>";
+    auto Rest = llvm::makeArrayRef(Refs);
+    unsigned End = 0;
+    for (unsigned I = 0; I < Code.size(); ++I) {
+      if (End == I && I > 0) {
+        OS << "</span>";
+        End = 0;
+      }
+      std::string TargetList;
+      Rest = Rest.drop_while([&](auto &R) {
+        if (R.first != I)
+          return false;
+        if (!TargetList.empty())
+          TargetList.push_back(',');
+        TargetList.push_back('t');
+        TargetList.append(std::to_string(R.second));
+        return true;
+      });
+      if (!TargetList.empty()) {
+        assert(End == 0 && "Overlapping tokens!");
+        OS << "<span class='ref sel' data-hover='" << TargetList << "'>";
+        End = I + Lexer::MeasureTokenLength(SM.getComposedLoc(File, I), SM,
+                                            Ctx.getLangOpts());
+      }
+      escapeChar(Code[I]);
+    }
+    OS << "</pre>\n";
+  }
+};
+
+} // namespace
+
+void writeHTMLReport(FileID File, llvm::ArrayRef<Decl *> Roots, ASTContext &Ctx,
+                     llvm::raw_ostream &OS) {
+  Reporter R(OS, Ctx, File);
+  for (Decl *Root : Roots)
+    walkAST(*Root,
+            [&](SourceLocation Loc, const NamedDecl &D) { R.addRef(Loc, D); });
+  R.write();
+}
+
+} // namespace clang::include_cleaner

diff  --git a/clang-tools-extra/include-cleaner/lib/Record.cpp b/clang-tools-extra/include-cleaner/lib/Record.cpp
new file mode 100644
index 0000000000000..88f9268eeb93c
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/lib/Record.cpp
@@ -0,0 +1,39 @@
+//===--- Record.cpp - Record compiler events ------------------------------===//
+//
+// 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 "clang-include-cleaner/Record.h"
+#include "clang/AST/ASTConsumer.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/DeclGroup.h"
+#include "clang/Basic/SourceManager.h"
+
+namespace clang::include_cleaner {
+
+std::unique_ptr<ASTConsumer> RecordedAST::record() {
+  class Recorder : public ASTConsumer {
+    RecordedAST *Out;
+
+  public:
+    Recorder(RecordedAST *Out) : Out(Out) {}
+    void Initialize(ASTContext &Ctx) override { Out->Ctx = &Ctx; }
+    bool HandleTopLevelDecl(DeclGroupRef DG) override {
+      const auto &SM = Out->Ctx->getSourceManager();
+      for (Decl *D : DG) {
+        if (!SM.isWrittenInMainFile(SM.getExpansionLoc(D->getLocation())))
+          continue;
+        // FIXME: Filter out certain Obj-C and template-related decls.
+        Out->Roots.push_back(D);
+      }
+      return ASTConsumer::HandleTopLevelDecl(DG);
+    }
+  };
+
+  return std::make_unique<Recorder>(this);
+}
+
+} // namespace clang::include_cleaner

diff  --git a/clang-tools-extra/include-cleaner/test/Inputs/bar.h b/clang-tools-extra/include-cleaner/test/Inputs/bar.h
new file mode 100644
index 0000000000000..ee50eedd6d459
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/Inputs/bar.h
@@ -0,0 +1,6 @@
+#ifndef BAR_H
+#define BAR_H
+
+int bar();
+
+#endif

diff  --git a/clang-tools-extra/include-cleaner/test/Inputs/foo.h b/clang-tools-extra/include-cleaner/test/Inputs/foo.h
new file mode 100644
index 0000000000000..4ec598ad513eb
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/Inputs/foo.h
@@ -0,0 +1,6 @@
+#ifndef FOO_H
+#define FOO_H
+
+int foo();
+
+#endif

diff  --git a/clang-tools-extra/include-cleaner/test/html.cpp b/clang-tools-extra/include-cleaner/test/html.cpp
new file mode 100644
index 0000000000000..127ad54c923fe
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/test/html.cpp
@@ -0,0 +1,6 @@
+// RUN: clang-include-cleaner -html=- %s -- -I %S/Inputs | FileCheck %s
+#include "bar.h"
+#include "foo.h"
+
+int n = foo();
+// CHECK: <span class='ref sel' data-hover='t{{[0-9]+}}'>foo</span>()

diff  --git a/clang-tools-extra/include-cleaner/tool/CMakeLists.txt b/clang-tools-extra/include-cleaner/tool/CMakeLists.txt
new file mode 100644
index 0000000000000..bd407aec847d9
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/tool/CMakeLists.txt
@@ -0,0 +1,12 @@
+set(LLVM_LINK_COMPONENTS Support)
+
+include_directories("../lib") # FIXME: use public APIs instead.
+add_clang_tool(clang-include-cleaner IncludeCleaner.cpp)
+clang_target_link_libraries(clang-include-cleaner PRIVATE
+  clangBasic
+  clangTooling
+  )
+target_link_libraries(clang-include-cleaner PRIVATE
+  clangIncludeCleaner
+  )
+

diff  --git a/clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp b/clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp
new file mode 100644
index 0000000000000..6d545c5e08734
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp
@@ -0,0 +1,100 @@
+//===--- IncludeCleaner.cpp - standalone tool for include analysis --------===//
+//
+// 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 "AnalysisInternal.h"
+#include "clang-include-cleaner/Record.h"
+#include "clang/Frontend/CompilerInstance.h"
+#include "clang/Frontend/FrontendAction.h"
+#include "clang/Tooling/CommonOptionsParser.h"
+#include "clang/Tooling/Tooling.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/CommandLine.h"
+#include "llvm/Support/Signals.h"
+#include "llvm/Support/raw_ostream.h"
+
+namespace clang {
+namespace include_cleaner {
+namespace {
+namespace cl = llvm::cl;
+
+llvm::StringRef Overview = llvm::StringLiteral(R"(
+clang-include-cleaner analyzes the #include directives in source code.
+
+It suggests removing headers that the code is not using.
+It suggests inserting headers that the code relies on, but does not include.
+These changes make the file more self-contained and (at scale) make the codebase
+easier to reason about and modify.
+
+The tool operates on *working* source code. This means it can suggest including
+headers that are only indirectly included, but cannot suggest those that are
+missing entirely. (clang-include-fixer can do this).
+)")
+                               .trim();
+
+cl::OptionCategory IncludeCleaner("clang-include-cleaner");
+
+cl::opt<std::string> HTMLReportPath{
+    "html",
+    cl::desc("Specify an output filename for an HTML report. "
+             "This describes both recommendations and reasons for changes."),
+    cl::cat(IncludeCleaner),
+};
+
+class HTMLReportAction : public clang::ASTFrontendAction {
+  RecordedAST AST;
+
+  std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
+                                                 StringRef File) override {
+    return AST.record();
+  }
+
+  void EndSourceFile() override {
+    std::error_code EC;
+    llvm::raw_fd_ostream OS(HTMLReportPath, EC);
+    if (EC) {
+      llvm::errs() << "Unable to write HTML report to " << HTMLReportPath
+                   << ": " << EC.message() << "\n";
+      exit(1);
+    }
+    writeHTMLReport(AST.Ctx->getSourceManager().getMainFileID(), AST.Roots,
+                    *AST.Ctx, OS);
+  }
+};
+
+} // namespace
+} // namespace include_cleaner
+} // namespace clang
+
+int main(int argc, const char **argv) {
+  using namespace clang::include_cleaner;
+
+  llvm::sys::PrintStackTraceOnErrorSignal(argv[0]);
+  auto OptionsParser =
+      clang::tooling::CommonOptionsParser::create(argc, argv, IncludeCleaner);
+  if (!OptionsParser) {
+    llvm::errs() << toString(OptionsParser.takeError());
+    return 1;
+  }
+
+  std::unique_ptr<clang::tooling::FrontendActionFactory> Factory;
+  if (HTMLReportPath.getNumOccurrences()) {
+    if (OptionsParser->getSourcePathList().size() != 1) {
+      llvm::errs() << "-" << HTMLReportPath.ArgStr
+                   << " requires a single input file";
+      return 1;
+    }
+    Factory = clang::tooling::newFrontendActionFactory<HTMLReportAction>();
+  } else {
+    llvm::errs() << "Unimplemented\n";
+    return 1;
+  }
+
+  return clang::tooling::ClangTool(OptionsParser->getCompilations(),
+                                   OptionsParser->getSourcePathList())
+      .run(Factory.get());
+}

diff  --git a/clang-tools-extra/include-cleaner/unittests/CMakeLists.txt b/clang-tools-extra/include-cleaner/unittests/CMakeLists.txt
index 8786f6d38e35f..7167d9214427d 100644
--- a/clang-tools-extra/include-cleaner/unittests/CMakeLists.txt
+++ b/clang-tools-extra/include-cleaner/unittests/CMakeLists.txt
@@ -5,6 +5,7 @@ set(LLVM_LINK_COMPONENTS
 
 add_custom_target(ClangIncludeCleanerUnitTests)
 add_unittest(ClangIncludeCleanerUnitTests ClangIncludeCleanerTests
+  RecordTest.cpp
   WalkASTTest.cpp
 )
 

diff  --git a/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp b/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
new file mode 100644
index 0000000000000..898ac1b526839
--- /dev/null
+++ b/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
@@ -0,0 +1,84 @@
+#include "clang-include-cleaner/Record.h"
+#include "clang/Frontend/FrontendAction.h"
+#include "clang/Testing/TestAST.h"
+#include "llvm/Support/raw_ostream.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace clang::include_cleaner {
+namespace {
+
+// Matches a Decl* if it is a NamedDecl with the given name.
+MATCHER_P(Named, N, "") {
+  if (const NamedDecl *ND = llvm::dyn_cast<NamedDecl>(arg)) {
+    if (N == ND->getNameAsString())
+      return true;
+  }
+  std::string S;
+  llvm::raw_string_ostream OS(S);
+  arg->dump(OS);
+  *result_listener << S;
+  return false;
+}
+
+class RecordASTTest : public ::testing::Test {
+protected:
+  TestInputs Inputs;
+  RecordedAST Recorded;
+
+  RecordASTTest() {
+    struct RecordAction : public ASTFrontendAction {
+      RecordedAST &Out;
+      RecordAction(RecordedAST &Out) : Out(Out) {}
+      std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
+                                                     StringRef) override {
+        return Out.record();
+      }
+    };
+    Inputs.MakeAction = [this] {
+      return std::make_unique<RecordAction>(Recorded);
+    };
+  }
+
+  TestAST build() { return TestAST(Inputs); }
+};
+
+// Top-level decl from the main file is a root, nested ones aren't.
+TEST_F(RecordASTTest, Namespace) {
+  Inputs.Code =
+      R"cpp(
+      namespace ns {
+        int x;
+        namespace {
+          int y;
+        }
+      }
+    )cpp";
+  auto AST = build();
+  EXPECT_THAT(Recorded.Roots, testing::ElementsAre(Named("ns")));
+}
+
+// Decl in included file is not a root.
+TEST_F(RecordASTTest, Inclusion) {
+  Inputs.ExtraFiles["header.h"] = "void headerFunc();";
+  Inputs.Code = R"cpp(
+    #include "header.h"
+    void mainFunc();
+  )cpp";
+  auto AST = build();
+  EXPECT_THAT(Recorded.Roots, testing::ElementsAre(Named("mainFunc")));
+}
+
+// Decl from macro expanded into the main file is a root.
+TEST_F(RecordASTTest, Macros) {
+  Inputs.ExtraFiles["header.h"] = "#define X void x();";
+  Inputs.Code = R"cpp(
+    #include "header.h"
+    X
+  )cpp";
+  auto AST = build();
+  EXPECT_THAT(Recorded.Roots, testing::ElementsAre(Named("x")));
+}
+
+} // namespace
+} // namespace clang::include_cleaner

diff  --git a/clang/include/clang/Testing/TestAST.h b/clang/include/clang/Testing/TestAST.h
index 9ed5fe0a0c038..7ba2ca882b91c 100644
--- a/clang/include/clang/Testing/TestAST.h
+++ b/clang/include/clang/Testing/TestAST.h
@@ -53,6 +53,10 @@ struct TestInputs {
   /// To suppress this, set ErrorOK or include "error-ok" in a comment in Code.
   /// In either case, all diagnostics appear in TestAST::diagnostics().
   bool ErrorOK = false;
+
+  /// The action used to parse the code.
+  /// By default, a SyntaxOnlyAction is used.
+  std::function<std::unique_ptr<FrontendAction>()> MakeAction;
 };
 
 /// The result of parsing a file specified by TestInputs.
@@ -78,6 +82,7 @@ class TestAST {
   SourceManager &sourceManager() { return Clang->getSourceManager(); }
   FileManager &fileManager() { return Clang->getFileManager(); }
   Preprocessor &preprocessor() { return Clang->getPreprocessor(); }
+  FrontendAction &action() { return *Action; }
 
   /// Returns diagnostics emitted during parsing.
   /// (By default, errors cause test failures, see TestInputs::ErrorOK).

diff  --git a/clang/lib/Testing/TestAST.cpp b/clang/lib/Testing/TestAST.cpp
index 5817230f4b098..8c79fcd7d6363 100644
--- a/clang/lib/Testing/TestAST.cpp
+++ b/clang/lib/Testing/TestAST.cpp
@@ -114,7 +114,8 @@ TestAST::TestAST(const TestInputs &In) {
   // Running the FrontendAction creates the other components: SourceManager,
   // Preprocessor, ASTContext, Sema. Preprocessor needs TargetInfo to be set.
   EXPECT_TRUE(Clang->createTarget());
-  Action = std::make_unique<SyntaxOnlyAction>();
+  Action =
+      In.MakeAction ? In.MakeAction() : std::make_unique<SyntaxOnlyAction>();
   const FrontendInputFile &Main = Clang->getFrontendOpts().Inputs.front();
   if (!Action->BeginSourceFile(*Clang, Main)) {
     ADD_FAILURE() << "Failed to BeginSourceFile()";


        


More information about the cfe-commits mailing list