//===--- FindHeadersTest.cpp ----------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "AnalysisInternal.h"
#include "TypesInternal.h"
#include "clang-include-cleaner/Analysis.h"
#include "clang-include-cleaner/Record.h"
#include "clang-include-cleaner/Types.h"
#include "clang/AST/Expr.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Basic/FileEntry.h"
#include "clang/Basic/FileManager.h"
#include "clang/Basic/LLVM.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/Testing/TestAST.h"
#include "clang/Tooling/Inclusions/StandardLibrary.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringRef.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <cassert>
#include <memory>

namespace clang::include_cleaner {
namespace {
using testing::ElementsAre;
using testing::UnorderedElementsAre;

std::string guard(llvm::StringRef Code) {
  return "#pragma once\n" + Code.str();
}

class FindHeadersTest : public testing::Test {
protected:
  TestInputs Inputs;
  PragmaIncludes PI;
  std::unique_ptr<TestAST> AST;
  FindHeadersTest() {
    Inputs.MakeAction = [this] {
      struct Hook : public SyntaxOnlyAction {
      public:
        Hook(PragmaIncludes *Out) : Out(Out) {}
        bool BeginSourceFileAction(clang::CompilerInstance &CI) override {
          Out->record(CI);
          return true;
        }

        PragmaIncludes *Out;
      };
      return std::make_unique<Hook>(&PI);
    };
  }
  void buildAST() { AST = std::make_unique<TestAST>(Inputs); }

  llvm::SmallVector<Hinted<Header>> findHeaders(llvm::StringRef FileName) {
    return include_cleaner::findHeaders(
        AST->sourceManager().translateFileLineCol(
            *AST->fileManager().getOptionalFileRef(FileName),
            /*Line=*/1, /*Col=*/1),
        AST->sourceManager(), &PI);
  }
  FileEntryRef physicalHeader(llvm::StringRef FileName) {
    return *AST->fileManager().getOptionalFileRef(FileName);
  };
};

TEST_F(FindHeadersTest, IWYUPrivateToPublic) {
  Inputs.Code = R"cpp(
    #include "private.h"
  )cpp";
  Inputs.ExtraFiles["private.h"] = guard(R"cpp(
    // IWYU pragma: private, include "path/public.h"
  )cpp");
  buildAST();
  EXPECT_THAT(findHeaders("private.h"),
              UnorderedElementsAre(physicalHeader("private.h"),
                                   Header("\"path/public.h\"")));
}

TEST_F(FindHeadersTest, IWYUExport) {
  Inputs.Code = R"cpp(
    #include "exporter.h"
  )cpp";
  Inputs.ExtraFiles["exporter.h"] = guard(R"cpp(
    #include "exported1.h" // IWYU pragma: export

    // IWYU pragma: begin_exports
    #include "exported2.h"
    // IWYU pragma: end_exports

    #include "normal.h"
  )cpp");
  Inputs.ExtraFiles["exported1.h"] = guard("");
  Inputs.ExtraFiles["exported2.h"] = guard("");
  Inputs.ExtraFiles["normal.h"] = guard("");

  buildAST();
  EXPECT_THAT(findHeaders("exported1.h"),
              UnorderedElementsAre(physicalHeader("exported1.h"),
                                   physicalHeader("exporter.h")));
  EXPECT_THAT(findHeaders("exported2.h"),
              UnorderedElementsAre(physicalHeader("exported2.h"),
                                   physicalHeader("exporter.h")));
  EXPECT_THAT(findHeaders("normal.h"),
              UnorderedElementsAre(physicalHeader("normal.h")));
  EXPECT_THAT(findHeaders("exporter.h"),
              UnorderedElementsAre(physicalHeader("exporter.h")));
}

TEST_F(FindHeadersTest, IWYUExportForStandardHeaders) {
  Inputs.Code = R"cpp(
    #include "exporter.h"
  )cpp";
  Inputs.ExtraFiles["exporter.h"] = guard(R"cpp(
    #include <string> // IWYU pragma: export
  )cpp");
  Inputs.ExtraFiles["string"] = guard("");
  Inputs.ExtraArgs.push_back("-isystem.");
  buildAST();
  tooling::stdlib::Symbol StdString =
      *tooling::stdlib::Symbol::named("std::", "string");
  EXPECT_THAT(
      include_cleaner::findHeaders(StdString, AST->sourceManager(), &PI),
      UnorderedElementsAre(physicalHeader("exporter.h"), StdString.header()));
}

TEST_F(FindHeadersTest, SelfContained) {
  Inputs.Code = R"cpp(
    #include "header.h"
  )cpp";
  Inputs.ExtraFiles["header.h"] = guard(R"cpp(
    #include "fragment.inc"
  )cpp");
  Inputs.ExtraFiles["fragment.inc"] = "";
  buildAST();
  EXPECT_THAT(findHeaders("fragment.inc"),
              UnorderedElementsAre(physicalHeader("fragment.inc"),
                                   physicalHeader("header.h")));
}

TEST_F(FindHeadersTest, NonSelfContainedTraversePrivate) {
  Inputs.Code = R"cpp(
    #include "header.h"
  )cpp";
  Inputs.ExtraFiles["header.h"] = guard(R"cpp(
    #include "fragment.inc"
  )cpp");
  Inputs.ExtraFiles["fragment.inc"] = R"cpp(
    // IWYU pragma: private, include "public.h"
  )cpp";

  buildAST();
  // There is a IWYU private mapping in the non self-contained header, verify
  // that we don't emit its includer.
  EXPECT_THAT(findHeaders("fragment.inc"),
              UnorderedElementsAre(physicalHeader("fragment.inc"),
                                   Header("\"public.h\"")));
}

TEST_F(FindHeadersTest, NonSelfContainedTraverseExporter) {
  Inputs.Code = R"cpp(
    #include "exporter.h"
  )cpp";
  Inputs.ExtraFiles["exporter.h"] = guard(R"cpp(
    #include "exported.h" // IWYU pragma: export
  )cpp");
  Inputs.ExtraFiles["exported.h"] = guard(R"cpp(
    #include "fragment.inc"
  )cpp");
  Inputs.ExtraFiles["fragment.inc"] = "";
  buildAST();
  // Verify that we emit exporters for each header on the path.
  EXPECT_THAT(findHeaders("fragment.inc"),
              UnorderedElementsAre(physicalHeader("fragment.inc"),
                                   physicalHeader("exported.h"),
                                   physicalHeader("exporter.h")));
}

TEST_F(FindHeadersTest, TargetIsExpandedFromMacroInHeader) {
  struct CustomVisitor : RecursiveASTVisitor<CustomVisitor> {
    const Decl *Out = nullptr;
    bool VisitNamedDecl(const NamedDecl *ND) {
      if (ND->getName() == "FLAG_foo" || ND->getName() == "Foo") {
        EXPECT_TRUE(Out == nullptr);
        Out = ND;
      }
      return true;
    }
  };

  struct {
    llvm::StringRef MacroHeader;
    llvm::StringRef DeclareHeader;
  } TestCases[] = {
      {/*MacroHeader=*/R"cpp(
    #define DEFINE_CLASS(name) class name {};
  )cpp",
       /*DeclareHeader=*/R"cpp(
    #include "macro.h"
    DEFINE_CLASS(Foo)
  )cpp"},
      {/*MacroHeader=*/R"cpp(
    #define DEFINE_Foo class Foo {};
  )cpp",
       /*DeclareHeader=*/R"cpp(
    #include "macro.h"
    DEFINE_Foo
  )cpp"},
      {/*MacroHeader=*/R"cpp(
    #define DECLARE_FLAGS(name) extern int FLAG_##name
  )cpp",
       /*DeclareHeader=*/R"cpp(
    #include "macro.h"
    DECLARE_FLAGS(foo);
  )cpp"},
  };

  for (const auto &T : TestCases) {
    Inputs.Code = R"cpp(#include "declare.h")cpp";
    Inputs.ExtraFiles["declare.h"] = guard(T.DeclareHeader);
    Inputs.ExtraFiles["macro.h"] = guard(T.MacroHeader);
    buildAST();

    CustomVisitor Visitor;
    Visitor.TraverseDecl(AST->context().getTranslationUnitDecl());

    auto Headers = clang::include_cleaner::findHeaders(
        Visitor.Out->getLocation(), AST->sourceManager(),
        /*PragmaIncludes=*/nullptr);
    EXPECT_THAT(Headers, UnorderedElementsAre(physicalHeader("declare.h")));
  }
}

MATCHER_P2(HintedHeader, Header, Hint, "") {
  return std::tie(arg.Hint, arg) == std::tie(Hint, Header);
}

TEST_F(FindHeadersTest, PublicHeaderHint) {
  Inputs.Code = R"cpp(
    #include "public.h"
  )cpp";
  Inputs.ExtraFiles["public.h"] = guard(R"cpp(
    #include "private.h"
    #include "private.inc"
  )cpp");
  Inputs.ExtraFiles["private.h"] = guard(R"cpp(
    // IWYU pragma: private
  )cpp");
  Inputs.ExtraFiles["private.inc"] = "";
  buildAST();
  // Non self-contained files and headers marked with IWYU private pragma
  // shouldn't have PublicHeader hint.
  EXPECT_THAT(
      findHeaders("private.inc"),
      UnorderedElementsAre(
          HintedHeader(physicalHeader("private.inc"), Hints::OriginHeader),
          HintedHeader(physicalHeader("public.h"), Hints::PublicHeader)));
  EXPECT_THAT(findHeaders("private.h"),
              UnorderedElementsAre(HintedHeader(physicalHeader("private.h"),
                                                Hints::OriginHeader)));
}

TEST_F(FindHeadersTest, PreferredHeaderHint) {
  Inputs.Code = R"cpp(
    #include "private.h"
  )cpp";
  Inputs.ExtraFiles["private.h"] = guard(R"cpp(
    // IWYU pragma: private, include "public.h"
  )cpp");
  buildAST();
  // Headers explicitly marked should've preferred signal.
  EXPECT_THAT(
      findHeaders("private.h"),
      UnorderedElementsAre(
          HintedHeader(physicalHeader("private.h"), Hints::OriginHeader),
          HintedHeader(Header("\"public.h\""),
                       Hints::PreferredHeader | Hints::PublicHeader)));
}

class HeadersForSymbolTest : public FindHeadersTest {
protected:
  llvm::SmallVector<Header> headersFor(llvm::StringRef Name) {
    struct Visitor : public RecursiveASTVisitor<Visitor> {
      const NamedDecl *Out = nullptr;
      llvm::StringRef Name;
      Visitor(llvm::StringRef Name) : Name(Name) {}
      bool VisitNamedDecl(const NamedDecl *ND) {
        if (auto *TD = ND->getDescribedTemplate())
          ND = TD;

        if (ND->getName() == Name) {
          EXPECT_TRUE(Out == nullptr || Out == ND->getCanonicalDecl())
              << "Found multiple matches for " << Name << ".";
          Out = cast<NamedDecl>(ND->getCanonicalDecl());
        }
        return true;
      }
    };
    Visitor V(Name);
    V.TraverseDecl(AST->context().getTranslationUnitDecl());
    if (!V.Out)
      ADD_FAILURE() << "Couldn't find any decls named " << Name << ".";
    assert(V.Out);
    return headersForSymbol(*V.Out, AST->preprocessor(), &PI);
  }
  llvm::SmallVector<Header> headersForFoo() { return headersFor("foo"); }
};

TEST_F(HeadersForSymbolTest, Deduplicates) {
  Inputs.Code = R"cpp(
    #include "foo.h"
  )cpp";
  Inputs.ExtraFiles["foo.h"] = guard(R"cpp(
    // IWYU pragma: private, include "foo.h"
    void foo();
    void foo();
  )cpp");
  buildAST();
  EXPECT_THAT(
      headersForFoo(),
      UnorderedElementsAre(physicalHeader("foo.h"),
                           // FIXME: de-duplicate across different kinds.
                           Header("\"foo.h\"")));
}

TEST_F(HeadersForSymbolTest, RankByName) {
  Inputs.Code = R"cpp(
    #include "fox.h"
    #include "bar.h"
  )cpp";
  Inputs.ExtraFiles["fox.h"] = guard(R"cpp(
    void foo();
  )cpp");
  Inputs.ExtraFiles["bar.h"] = guard(R"cpp(
    void foo();
  )cpp");
  buildAST();
  EXPECT_THAT(headersForFoo(),
              ElementsAre(physicalHeader("bar.h"), physicalHeader("fox.h")));
}

TEST_F(HeadersForSymbolTest, Ranking) {
  // Sorting is done over (public, complete, canonical, origin)-tuple.
  Inputs.Code = R"cpp(
    #include "private.h"
    #include "public.h"
    #include "public_complete.h"
    #include "exporter.h"
  )cpp";
  Inputs.ExtraFiles["public.h"] = guard(R"cpp(
    struct foo;
  )cpp");
  Inputs.ExtraFiles["private.h"] = guard(R"cpp(
    // IWYU pragma: private, include "canonical.h"
    struct foo;
  )cpp");
  Inputs.ExtraFiles["exporter.h"] = guard(R"cpp(
  #include "private.h" // IWYU pragma: export
  )cpp");
  Inputs.ExtraFiles["public_complete.h"] = guard("struct foo {};");
  buildAST();
  EXPECT_THAT(headersForFoo(),
              ElementsAre(physicalHeader("public_complete.h"),
                          Header("\"canonical.h\""), physicalHeader("public.h"),
                          physicalHeader("exporter.h"),
                          physicalHeader("private.h")));
}

TEST_F(HeadersForSymbolTest, PreferPublicOverComplete) {
  Inputs.Code = R"cpp(
    #include "complete_private.h"
    #include "public.h"
  )cpp";
  Inputs.ExtraFiles["complete_private.h"] = guard(R"cpp(
    // IWYU pragma: private
    struct foo {};
  )cpp");
  Inputs.ExtraFiles["public.h"] = guard("struct foo;");
  buildAST();
  EXPECT_THAT(headersForFoo(),
              ElementsAre(physicalHeader("public.h"),
                          physicalHeader("complete_private.h")));
}

TEST_F(HeadersForSymbolTest, PreferNameMatch) {
  Inputs.Code = R"cpp(
    #include "public_complete.h"
    #include "test/foo.fwd.h"
  )cpp";
  Inputs.ExtraFiles["public_complete.h"] = guard("struct foo {};");
  Inputs.ExtraFiles["test/foo.fwd.h"] = guard("struct foo;");
  buildAST();
  EXPECT_THAT(headersForFoo(), ElementsAre(physicalHeader("public_complete.h"),
                                           physicalHeader("test/foo.fwd.h")));
}

TEST_F(HeadersForSymbolTest, MainFile) {
  Inputs.Code = R"cpp(
    #include "public_complete.h"
    struct foo;
  )cpp";
  Inputs.ExtraFiles["public_complete.h"] = guard(R"cpp(
    struct foo {};
  )cpp");
  buildAST();
  auto &SM = AST->sourceManager();
  // FIXME: Symbols provided by main file should be treated specially.
  EXPECT_THAT(
      headersForFoo(),
      ElementsAre(physicalHeader("public_complete.h"),
                  Header(*SM.getFileEntryRefForID(SM.getMainFileID()))));
}

TEST_F(HeadersForSymbolTest, PreferExporterOfPrivate) {
  Inputs.Code = R"cpp(
    #include "private.h"
    #include "exporter.h"
  )cpp";
  Inputs.ExtraFiles["private.h"] = guard(R"cpp(
    // IWYU pragma: private
    struct foo {};
  )cpp");
  Inputs.ExtraFiles["exporter.h"] = guard(R"cpp(
    #include "private.h" // IWYU pragma: export
  )cpp");
  buildAST();
  EXPECT_THAT(headersForFoo(), ElementsAre(physicalHeader("exporter.h"),
                                           physicalHeader("private.h")));
}

TEST_F(HeadersForSymbolTest, ExporterIsDownRanked) {
  Inputs.Code = R"cpp(
    #include "exporter.h"
    #include "zoo.h"
  )cpp";
  // Deliberately named as zoo to make sure it doesn't get name-match boost and
  // also gets lexicographically bigger order than "exporter".
  Inputs.ExtraFiles["zoo.h"] = guard(R"cpp(
    struct foo {};
  )cpp");
  Inputs.ExtraFiles["exporter.h"] = guard(R"cpp(
    #include "zoo.h" // IWYU pragma: export
  )cpp");
  buildAST();
  EXPECT_THAT(headersForFoo(), ElementsAre(physicalHeader("zoo.h"),
                                           physicalHeader("exporter.h")));
}

TEST_F(HeadersForSymbolTest, PreferPublicOverNameMatchOnPrivate) {
  Inputs.Code = R"cpp(
    #include "foo.h"
  )cpp";
  Inputs.ExtraFiles["foo.h"] = guard(R"cpp(
    // IWYU pragma: private, include "public.h"
    struct foo {};
  )cpp");
  buildAST();
  EXPECT_THAT(headersForFoo(), ElementsAre(Header(StringRef("\"public.h\"")),
                                           physicalHeader("foo.h")));
}

TEST_F(HeadersForSymbolTest, PublicOverPrivateWithoutUmbrella) {
  Inputs.Code = R"cpp(
    #include "bar.h"
    #include "foo.h"
  )cpp";
  Inputs.ExtraFiles["bar.h"] =
      guard(R"cpp(#include "foo.h" // IWYU pragma: export)cpp");
  Inputs.ExtraFiles["foo.h"] = guard(R"cpp(
    // IWYU pragma: private
    struct foo {};
  )cpp");
  buildAST();
  EXPECT_THAT(headersForFoo(),
              ElementsAre(physicalHeader("bar.h"), physicalHeader("foo.h")));
}

TEST_F(HeadersForSymbolTest, IWYUTransitiveExport) {
  Inputs.Code = R"cpp(
    #include "export1.h"
  )cpp";
  Inputs.ExtraFiles["export1.h"] = guard(R"cpp(
    #include "export2.h" // IWYU pragma: export
  )cpp");
  Inputs.ExtraFiles["export2.h"] = guard(R"cpp(
    #include "foo.h" // IWYU pragma: export
  )cpp");
  Inputs.ExtraFiles["foo.h"] = guard(R"cpp(
    struct foo {};
  )cpp");
  buildAST();
  EXPECT_THAT(headersForFoo(),
              ElementsAre(physicalHeader("foo.h"), physicalHeader("export1.h"),
                          physicalHeader("export2.h")));
}

TEST_F(HeadersForSymbolTest, IWYUTransitiveExportWithPrivate) {
  Inputs.Code = R"cpp(
    #include "export1.h"
    void bar() { foo();}
  )cpp";
  Inputs.ExtraFiles["export1.h"] = guard(R"cpp(
    // IWYU pragma: private, include "public1.h"
    #include "export2.h" // IWYU pragma: export
    void foo();
  )cpp");
  Inputs.ExtraFiles["export2.h"] = guard(R"cpp(
    // IWYU pragma: private, include "public2.h"
    #include "export3.h" // IWYU pragma: export
  )cpp");
  Inputs.ExtraFiles["export3.h"] = guard(R"cpp(
    // IWYU pragma: private, include "public3.h"
    #include "foo.h" // IWYU pragma: export
  )cpp");
  Inputs.ExtraFiles["foo.h"] = guard(R"cpp(
    void foo();
  )cpp");
  buildAST();
  EXPECT_THAT(headersForFoo(),
              ElementsAre(physicalHeader("foo.h"),
                                           Header(StringRef("\"public1.h\"")),
                                           physicalHeader("export1.h"),
                                           physicalHeader("export2.h"),
                                           physicalHeader("export3.h")));
}

TEST_F(HeadersForSymbolTest, AmbiguousStdSymbols) {
  struct {
    llvm::StringRef Code;
    llvm::StringRef Name;

    llvm::StringRef ExpectedHeader;
  } TestCases[] = {
      {
          R"cpp(
            namespace std {
             template <typename InputIt, typename OutputIt>
             constexpr OutputIt move(InputIt first, InputIt last, OutputIt dest);
            })cpp",
          "move",
          "<algorithm>",
      },
      {
          R"cpp(
            namespace std {
             template<class ExecutionPolicy, class ForwardIt1, class ForwardIt2>
             ForwardIt2 move(ExecutionPolicy&& policy,
                 ForwardIt1 first, ForwardIt1 last, ForwardIt2 d_first);
            })cpp",
          "move",
          "<algorithm>",
      },
      {
          R"cpp(
            namespace std {
              template<typename T> constexpr T move(T&& t) noexcept;
            })cpp",
          "move",
          "<utility>",
      },
      {
          R"cpp(
            namespace std {
              template<class ForwardIt, class T>
              ForwardIt remove(ForwardIt first, ForwardIt last, const T& value);
            })cpp",
          "remove",
          "<algorithm>",
      },
      {
          "namespace std { int remove(const char*); }",
          "remove",
          "<cstdio>",
      },
  };

  for (const auto &T : TestCases) {
    Inputs.Code = T.Code;
    buildAST();
    EXPECT_THAT(headersFor(T.Name),
                UnorderedElementsAre(
                    Header(*tooling::stdlib::Header::named(T.ExpectedHeader))));
  }
}

TEST_F(HeadersForSymbolTest, AmbiguousStdSymbolsUsingShadow) {
  Inputs.Code = R"cpp(
    void remove(char*);
    namespace std { using ::remove; }

    void k() {
      std::remove("abc");
    }
  )cpp";
  buildAST();

  // Find the DeclRefExpr in the std::remove("abc") function call.
  struct Visitor : public RecursiveASTVisitor<Visitor> {
    const DeclRefExpr *Out = nullptr;
    bool VisitDeclRefExpr(const DeclRefExpr *DRE) {
      EXPECT_TRUE(Out == nullptr) << "Found multiple DeclRefExpr!";
      Out = DRE;
      return true;
    }
  };
  Visitor V;
  V.TraverseDecl(AST->context().getTranslationUnitDecl());
  ASSERT_TRUE(V.Out) << "Couldn't find a DeclRefExpr!";
  EXPECT_THAT(
      headersForSymbol(*(V.Out->getFoundDecl()), AST->preprocessor(), &PI),
      UnorderedElementsAre(
          Header(*tooling::stdlib::Header::named("<cstdio>"))));
}

TEST_F(HeadersForSymbolTest, StandardHeaders) {
  Inputs.Code = R"cpp(
    #include "stdlib_internal.h"
    void assert();
    void foo() { assert(); }
  )cpp";
  Inputs.ExtraFiles["stdlib_internal.h"] = "void assert();";
  buildAST();
  EXPECT_THAT(
      headersFor("assert"),
      // Respect the ordering from the stdlib mapping.
      // FIXME: Report physical locations too, stdlib_internal.h and main-file
      // should also be candidates. But they should be down-ranked compared to
      // stdlib providers.
      UnorderedElementsAre(tooling::stdlib::Header::named("<cassert>"),
                           tooling::stdlib::Header::named("<assert.h>")));
}

TEST_F(HeadersForSymbolTest, StdlibLangForMacros) {
  Inputs.Code = R"cpp(
    #define EOF 0
    void foo() { EOF; }
  )cpp";
  {
    buildAST();
    const Macro Eof{AST->preprocessor().getIdentifierInfo("EOF"), {}};
    EXPECT_THAT(
        headersForSymbol(Eof, AST->preprocessor(), nullptr),
        UnorderedElementsAre(tooling::stdlib::Header::named("<cstdio>"),
                             tooling::stdlib::Header::named("<stdio.h>")));
  }

  {
    Inputs.ExtraArgs.push_back("-xc");
    buildAST();
    const Macro Eof{AST->preprocessor().getIdentifierInfo("EOF"), {}};
    EXPECT_THAT(headersForSymbol(Eof, AST->preprocessor(), nullptr),
                UnorderedElementsAre(tooling::stdlib::Header::named(
                    "<stdio.h>", tooling::stdlib::Lang::C)));
  }
}

TEST_F(HeadersForSymbolTest, ExporterNoNameMatch) {
  Inputs.Code = R"cpp(
    #include "exporter/foo.h"
    #include "foo_public.h"
  )cpp";
  Inputs.ExtraArgs.emplace_back("-I.");
  // Deliberately named as foo_public to make sure it doesn't get name-match
  // boost and also gets lexicographically bigger order than "exporter/foo.h".
  Inputs.ExtraFiles["foo_public.h"] = guard(R"cpp(
    struct foo {};
  )cpp");
  Inputs.ExtraFiles["exporter/foo.h"] = guard(R"cpp(
    #include "foo_public.h" // IWYU pragma: export
  )cpp");
  buildAST();
  EXPECT_THAT(headersForFoo(), ElementsAre(physicalHeader("foo_public.h"),
                                           physicalHeader("exporter/foo.h")));
}

} // namespace
} // namespace clang::include_cleaner
