//===--- AnalysisTest.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 "clang-include-cleaner/Analysis.h" #include "AnalysisInternal.h" #include "TypesInternal.h" #include "clang-include-cleaner/Record.h" #include "clang-include-cleaner/Types.h" #include "clang/AST/ASTContext.h" #include "clang/Basic/FileManager.h" #include "clang/Basic/IdentifierTable.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/SourceManager.h" #include "clang/Format/Format.h" #include "clang/Frontend/FrontendActions.h" #include "clang/Testing/TestAST.h" #include "clang/Tooling/Inclusions/StandardLibrary.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/ScopedPrinter.h" #include "llvm/Testing/Annotations/Annotations.h" #include "gmock/gmock.h" #include "gtest/gtest.h" #include #include #include #include #include namespace clang::include_cleaner { namespace { using testing::AllOf; using testing::Contains; using testing::ElementsAre; using testing::Pair; using testing::UnorderedElementsAre; std::string guard(llvm::StringRef Code) { return "#pragma once\n" + Code.str(); } class WalkUsedTest : public testing::Test { protected: TestInputs Inputs; PragmaIncludes PI; WalkUsedTest() { 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(&PI); }; } std::multimap> offsetToProviders(TestAST &AST, llvm::ArrayRef MacroRefs = {}) { const auto &SM = AST.sourceManager(); llvm::SmallVector TopLevelDecls; for (Decl *D : AST.context().getTranslationUnitDecl()->decls()) { if (!SM.isWrittenInMainFile(SM.getExpansionLoc(D->getLocation()))) continue; TopLevelDecls.emplace_back(D); } std::multimap> OffsetToProviders; walkUsed(TopLevelDecls, MacroRefs, &PI, AST.preprocessor(), [&](const SymbolReference &Ref, llvm::ArrayRef
Providers) { auto [FID, Offset] = SM.getDecomposedLoc(Ref.RefLocation); if (FID != SM.getMainFileID()) ADD_FAILURE() << "Reference outside of the main file!"; OffsetToProviders.emplace(Offset, Providers.vec()); }); return OffsetToProviders; } }; TEST_F(WalkUsedTest, Basic) { llvm::Annotations Code(R"cpp( #include "header.h" #include "private.h" // No reference reported for the Parameter "p". void $bar^bar($private^Private p) { $foo^foo(); std::$vector^vector $vconstructor^$v^v; $builtin^__builtin_popcount(1); std::$move^move(3); } )cpp"); Inputs.Code = Code.code(); Inputs.ExtraFiles["header.h"] = guard(R"cpp( void foo(); namespace std { class vector {}; int&& move(int&&); } )cpp"); Inputs.ExtraFiles["private.h"] = guard(R"cpp( // IWYU pragma: private, include "path/public.h" class Private {}; )cpp"); TestAST AST(Inputs); auto &SM = AST.sourceManager(); auto HeaderFile = Header(*AST.fileManager().getOptionalFileRef("header.h")); auto PrivateFile = Header(*AST.fileManager().getOptionalFileRef("private.h")); auto PublicFile = Header("\"path/public.h\""); auto MainFile = Header(*SM.getFileEntryRefForID(SM.getMainFileID())); auto VectorSTL = Header(*tooling::stdlib::Header::named("")); auto UtilitySTL = Header(*tooling::stdlib::Header::named("")); EXPECT_THAT( offsetToProviders(AST), UnorderedElementsAre( Pair(Code.point("bar"), UnorderedElementsAre(MainFile)), Pair(Code.point("private"), UnorderedElementsAre(PublicFile, PrivateFile)), Pair(Code.point("foo"), UnorderedElementsAre(HeaderFile)), Pair(Code.point("vector"), UnorderedElementsAre(VectorSTL)), Pair(Code.point("vconstructor"), UnorderedElementsAre(VectorSTL)), Pair(Code.point("v"), UnorderedElementsAre(MainFile)), Pair(Code.point("builtin"), testing::IsEmpty()), Pair(Code.point("move"), UnorderedElementsAre(UtilitySTL)))); } TEST_F(WalkUsedTest, MultipleProviders) { llvm::Annotations Code(R"cpp( #include "header1.h" #include "header2.h" void foo(); void bar() { $foo^foo(); } )cpp"); Inputs.Code = Code.code(); Inputs.ExtraFiles["header1.h"] = guard(R"cpp( void foo(); )cpp"); Inputs.ExtraFiles["header2.h"] = guard(R"cpp( void foo(); )cpp"); TestAST AST(Inputs); auto &SM = AST.sourceManager(); auto HeaderFile1 = Header(*AST.fileManager().getOptionalFileRef("header1.h")); auto HeaderFile2 = Header(*AST.fileManager().getOptionalFileRef("header2.h")); auto MainFile = Header(*SM.getFileEntryRefForID(SM.getMainFileID())); EXPECT_THAT( offsetToProviders(AST), Contains(Pair(Code.point("foo"), UnorderedElementsAre(HeaderFile1, HeaderFile2, MainFile)))); } TEST_F(WalkUsedTest, MacroRefs) { llvm::Annotations Code(R"cpp( #include "hdr.h" int $3^x = $1^ANSWER; int $4^y = $2^ANSWER; )cpp"); llvm::Annotations Hdr(guard("#define ^ANSWER 42")); Inputs.Code = Code.code(); Inputs.ExtraFiles["hdr.h"] = Hdr.code(); TestAST AST(Inputs); auto &SM = AST.sourceManager(); auto &PP = AST.preprocessor(); auto HdrFile = *SM.getFileManager().getOptionalFileRef("hdr.h"); auto MainFile = Header(*SM.getFileEntryRefForID(SM.getMainFileID())); auto HdrID = SM.translateFile(HdrFile); Symbol Answer1 = Macro{PP.getIdentifierInfo("ANSWER"), SM.getComposedLoc(HdrID, Hdr.point())}; Symbol Answer2 = Macro{PP.getIdentifierInfo("ANSWER"), SM.getComposedLoc(HdrID, Hdr.point())}; EXPECT_THAT( offsetToProviders( AST, {SymbolReference{ Answer1, SM.getComposedLoc(SM.getMainFileID(), Code.point("1")), RefType::Explicit}, SymbolReference{ Answer2, SM.getComposedLoc(SM.getMainFileID(), Code.point("2")), RefType::Explicit}}), UnorderedElementsAre( Pair(Code.point("1"), UnorderedElementsAre(HdrFile)), Pair(Code.point("2"), UnorderedElementsAre(HdrFile)), Pair(Code.point("3"), UnorderedElementsAre(MainFile)), Pair(Code.point("4"), UnorderedElementsAre(MainFile)))); } class AnalyzeTest : public testing::Test { protected: TestInputs Inputs; PragmaIncludes PI; RecordedPP PP; AnalyzeTest() { Inputs.MakeAction = [this] { struct Hook : public SyntaxOnlyAction { public: Hook(RecordedPP &PP, PragmaIncludes &PI) : PP(PP), PI(PI) {} bool BeginSourceFileAction(clang::CompilerInstance &CI) override { CI.getPreprocessor().addPPCallbacks(PP.record(CI.getPreprocessor())); PI.record(CI); return true; } RecordedPP &PP; PragmaIncludes &PI; }; return std::make_unique(PP, PI); }; } }; TEST_F(AnalyzeTest, Basic) { Inputs.Code = R"cpp( #include "a.h" #include "b.h" #include "keep.h" // IWYU pragma: keep int x = a + c; )cpp"; Inputs.ExtraFiles["a.h"] = guard("int a;"); Inputs.ExtraFiles["b.h"] = guard(R"cpp( #include "c.h" int b; )cpp"); Inputs.ExtraFiles["c.h"] = guard("int c;"); Inputs.ExtraFiles["keep.h"] = guard(""); TestAST AST(Inputs); auto Decls = AST.context().getTranslationUnitDecl()->decls(); auto Results = analyze(std::vector{Decls.begin(), Decls.end()}, PP.MacroReferences, PP.Includes, &PI, AST.preprocessor()); const Include *B = PP.Includes.atLine(3); ASSERT_EQ(B->Spelled, "b.h"); EXPECT_THAT(Results.Missing, ElementsAre("\"c.h\"")); EXPECT_THAT(Results.Unused, ElementsAre(B)); } TEST_F(AnalyzeTest, PrivateUsedInPublic) { // Check that umbrella header uses private include. Inputs.Code = R"cpp(#include "private.h")cpp"; Inputs.ExtraFiles["private.h"] = guard("// IWYU pragma: private, include \"public.h\""); Inputs.FileName = "public.h"; TestAST AST(Inputs); EXPECT_FALSE(PP.Includes.all().empty()); auto Results = analyze({}, {}, PP.Includes, &PI, AST.preprocessor()); EXPECT_THAT(Results.Unused, testing::IsEmpty()); } TEST_F(AnalyzeTest, NoCrashWhenUnresolved) { // Check that umbrella header uses private include. Inputs.Code = R"cpp(#include "not_found.h")cpp"; Inputs.ErrorOK = true; TestAST AST(Inputs); EXPECT_FALSE(PP.Includes.all().empty()); auto Results = analyze({}, {}, PP.Includes, &PI, AST.preprocessor()); EXPECT_THAT(Results.Unused, testing::IsEmpty()); } TEST_F(AnalyzeTest, ResourceDirIsIgnored) { Inputs.ExtraArgs.push_back("-resource-dir"); Inputs.ExtraArgs.push_back("resources"); Inputs.ExtraArgs.push_back("-internal-isystem"); Inputs.ExtraArgs.push_back("resources/include"); Inputs.Code = R"cpp( #include #include void baz() { bar(); } )cpp"; Inputs.ExtraFiles["resources/include/amintrin.h"] = guard(""); Inputs.ExtraFiles["resources/include/emintrin.h"] = guard(R"cpp( void bar(); )cpp"); Inputs.ExtraFiles["resources/include/imintrin.h"] = guard(R"cpp( #include )cpp"); TestAST AST(Inputs); auto Results = analyze({}, {}, PP.Includes, &PI, AST.preprocessor()); EXPECT_THAT(Results.Unused, testing::IsEmpty()); EXPECT_THAT(Results.Missing, testing::IsEmpty()); } TEST(FixIncludes, Basic) { llvm::StringRef Code = R"cpp(#include "d.h" #include "a.h" #include "b.h" #include )cpp"; Includes Inc; Include I; I.Spelled = "a.h"; I.Line = 2; Inc.add(I); I.Spelled = "b.h"; I.Line = 3; Inc.add(I); I.Spelled = "c.h"; I.Line = 4; I.Angled = true; Inc.add(I); AnalysisResults Results; Results.Missing.push_back("\"aa.h\""); Results.Missing.push_back("\"ab.h\""); Results.Missing.push_back(""); Results.Unused.push_back(Inc.atLine(3)); Results.Unused.push_back(Inc.atLine(4)); EXPECT_EQ(fixIncludes(Results, "d.cc", Code, format::getLLVMStyle()), R"cpp(#include "d.h" #include "a.h" #include "aa.h" #include "ab.h" #include )cpp"); Results = {}; Results.Missing.push_back("\"d.h\""); Code = R"cpp(#include "a.h")cpp"; EXPECT_EQ(fixIncludes(Results, "d.cc", Code, format::getLLVMStyle()), R"cpp(#include "d.h" #include "a.h")cpp"); } MATCHER_P3(expandedAt, FileID, Offset, SM, "") { auto [ExpanedFileID, ExpandedOffset] = SM->getDecomposedExpansionLoc(arg); return ExpanedFileID == FileID && ExpandedOffset == Offset; } MATCHER_P3(spelledAt, FileID, Offset, SM, "") { auto [SpelledFileID, SpelledOffset] = SM->getDecomposedSpellingLoc(arg); return SpelledFileID == FileID && SpelledOffset == Offset; } TEST(WalkUsed, FilterRefsNotSpelledInMainFile) { // Each test is expected to have a single expected ref of `target` symbol // (or have none). // The location in the reported ref is a macro location. $expand points to // the macro location, and $spell points to the spelled location. struct { llvm::StringRef Header; llvm::StringRef Main; } TestCases[] = { // Tests for decl references. { /*Header=*/"int target();", R"cpp( #define CALL_FUNC $spell^target() int b = $expand^CALL_FUNC; )cpp", }, {/*Header=*/R"cpp( int target(); #define CALL_FUNC target() )cpp", // No ref of `target` being reported, as it is not spelled in main file. "int a = CALL_FUNC;"}, { /*Header=*/R"cpp( int target(); #define PLUS_ONE(X) X() + 1 )cpp", R"cpp( int a = $expand^PLUS_ONE($spell^target); )cpp", }, { /*Header=*/R"cpp( int target(); #define PLUS_ONE(X) X() + 1 )cpp", R"cpp( int a = $expand^PLUS_ONE($spell^target); )cpp", }, // Tests for macro references {/*Header=*/"#define target 1", R"cpp( #define USE_target $spell^target int b = $expand^USE_target; )cpp"}, {/*Header=*/R"cpp( #define target 1 #define USE_target target )cpp", // No ref of `target` being reported, it is not spelled in main file. R"cpp( int a = USE_target; )cpp"}, }; for (const auto &T : TestCases) { llvm::Annotations Main(T.Main); TestInputs Inputs(Main.code()); Inputs.ExtraFiles["header.h"] = guard(T.Header); RecordedPP Recorded; Inputs.MakeAction = [&]() { struct RecordAction : public SyntaxOnlyAction { RecordedPP &Out; RecordAction(RecordedPP &Out) : Out(Out) {} bool BeginSourceFileAction(clang::CompilerInstance &CI) override { auto &PP = CI.getPreprocessor(); PP.addPPCallbacks(Out.record(PP)); return true; } }; return std::make_unique(Recorded); }; Inputs.ExtraArgs.push_back("-include"); Inputs.ExtraArgs.push_back("header.h"); TestAST AST(Inputs); llvm::SmallVector TopLevelDecls; for (Decl *D : AST.context().getTranslationUnitDecl()->decls()) TopLevelDecls.emplace_back(D); auto &SM = AST.sourceManager(); SourceLocation RefLoc; walkUsed(TopLevelDecls, Recorded.MacroReferences, /*PragmaIncludes=*/nullptr, AST.preprocessor(), [&](const SymbolReference &Ref, llvm::ArrayRef
) { if (!Ref.RefLocation.isMacroID()) return; if (llvm::to_string(Ref.Target) == "target") { ASSERT_TRUE(RefLoc.isInvalid()) << "Expected only one 'target' ref loc per testcase"; RefLoc = Ref.RefLocation; } }); FileID MainFID = SM.getMainFileID(); if (RefLoc.isValid()) { EXPECT_THAT(RefLoc, AllOf(expandedAt(MainFID, Main.point("expand"), &SM), spelledAt(MainFID, Main.point("spell"), &SM))) << T.Main.str(); } else { EXPECT_THAT(Main.points(), testing::IsEmpty()); } } } struct Tag { friend llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const Tag &T) { return OS << "Anon Tag"; } }; TEST(Hints, Ordering) { auto Hinted = [](Hints Hints) { return clang::include_cleaner::Hinted({}, Hints); }; EXPECT_LT(Hinted(Hints::None), Hinted(Hints::CompleteSymbol)); EXPECT_LT(Hinted(Hints::CompleteSymbol), Hinted(Hints::PublicHeader)); EXPECT_LT(Hinted(Hints::PreferredHeader), Hinted(Hints::PublicHeader)); EXPECT_LT(Hinted(Hints::CompleteSymbol | Hints::PreferredHeader), Hinted(Hints::PublicHeader)); } // Test ast traversal & redecl selection end-to-end for templates, as explicit // instantiations/specializations are not redecls of the primary template. We // need to make sure we're selecting the right ones. TEST_F(WalkUsedTest, TemplateDecls) { llvm::Annotations Code(R"cpp( #include "fwd.h" #include "def.h" #include "partial.h" template <> struct $exp_spec^Foo {}; template struct $exp^Foo; $full^Foo x; $implicit^Foo y; $partial^Foo z; )cpp"); Inputs.Code = Code.code(); Inputs.ExtraFiles["fwd.h"] = guard("template struct Foo;"); Inputs.ExtraFiles["def.h"] = guard("template struct Foo {};"); Inputs.ExtraFiles["partial.h"] = guard("template struct Foo {};"); TestAST AST(Inputs); auto &SM = AST.sourceManager(); auto Fwd = *SM.getFileManager().getOptionalFileRef("fwd.h"); auto Def = *SM.getFileManager().getOptionalFileRef("def.h"); auto Partial = *SM.getFileManager().getOptionalFileRef("partial.h"); EXPECT_THAT( offsetToProviders(AST), AllOf(Contains( Pair(Code.point("exp_spec"), UnorderedElementsAre(Fwd, Def))), Contains(Pair(Code.point("exp"), UnorderedElementsAre(Fwd, Def))), Contains(Pair(Code.point("full"), UnorderedElementsAre(Fwd, Def))), Contains( Pair(Code.point("implicit"), UnorderedElementsAre(Fwd, Def))), Contains( Pair(Code.point("partial"), UnorderedElementsAre(Partial))))); } TEST_F(WalkUsedTest, IgnoresIdentityMacros) { llvm::Annotations Code(R"cpp( #include "header.h" void $bar^bar() { $stdin^stdin(); } )cpp"); Inputs.Code = Code.code(); Inputs.ExtraFiles["header.h"] = guard(R"cpp( #include "inner.h" void stdin(); )cpp"); Inputs.ExtraFiles["inner.h"] = guard(R"cpp( #define stdin stdin )cpp"); TestAST AST(Inputs); auto &SM = AST.sourceManager(); auto MainFile = Header(*SM.getFileEntryRefForID(SM.getMainFileID())); EXPECT_THAT(offsetToProviders(AST), UnorderedElementsAre( // FIXME: we should have a reference from stdin to header.h Pair(Code.point("bar"), UnorderedElementsAre(MainFile)))); } } // namespace } // namespace clang::include_cleaner