1 //===-- RecordTest.cpp ----------------------------------------------------===//
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7 //===----------------------------------------------------------------------===//
9 #include "clang-include-cleaner/Record.h"
10 #include "clang/Basic/SourceLocation.h"
11 #include "clang/Frontend/FrontendAction.h"
12 #include "clang/Frontend/FrontendActions.h"
13 #include "clang/Testing/TestAST.h"
14 #include "clang/Tooling/Inclusions/StandardLibrary.h"
15 #include "llvm/ADT/ArrayRef.h"
16 #include "llvm/Support/raw_ostream.h"
17 #include "llvm/Testing/Annotations/Annotations.h"
18 #include "gmock/gmock.h"
19 #include "gtest/gtest.h"
21 namespace clang::include_cleaner
{
23 using testing::ElementsAreArray
;
24 using testing::IsEmpty
;
26 // Matches a Decl* if it is a NamedDecl with the given name.
27 MATCHER_P(named
, N
, "") {
28 if (const NamedDecl
*ND
= llvm::dyn_cast
<NamedDecl
>(arg
)) {
29 if (N
== ND
->getNameAsString())
33 llvm::raw_string_ostream
OS(S
);
35 *result_listener
<< S
;
39 MATCHER_P(FileNamed
, N
, "") {
40 if (arg
->tryGetRealPathName() == N
)
42 *result_listener
<< arg
->tryGetRealPathName().str();
46 class RecordASTTest
: public ::testing::Test
{
52 struct RecordAction
: public ASTFrontendAction
{
54 RecordAction(RecordedAST
&Out
) : Out(Out
) {}
55 std::unique_ptr
<ASTConsumer
> CreateASTConsumer(CompilerInstance
&CI
,
60 Inputs
.MakeAction
= [this] {
61 return std::make_unique
<RecordAction
>(Recorded
);
65 TestAST
build() { return TestAST(Inputs
); }
68 // Top-level decl from the main file is a root, nested ones aren't.
69 TEST_F(RecordASTTest
, Namespace
) {
80 EXPECT_THAT(Recorded
.Roots
, testing::ElementsAre(named("ns")));
83 // Decl in included file is not a root.
84 TEST_F(RecordASTTest
, Inclusion
) {
85 Inputs
.ExtraFiles
["header.h"] = "void headerFunc();";
91 EXPECT_THAT(Recorded
.Roots
, testing::ElementsAre(named("mainFunc")));
94 // Decl from macro expanded into the main file is a root.
95 TEST_F(RecordASTTest
, Macros
) {
96 Inputs
.ExtraFiles
["header.h"] = "#define X void x();";
102 EXPECT_THAT(Recorded
.Roots
, testing::ElementsAre(named("x")));
105 // Decl from template instantiation is filtered out from roots.
106 TEST_F(RecordASTTest
, ImplicitTemplates
) {
107 Inputs
.ExtraFiles
["dispatch.h"] = R
"cpp(
109 static constexpr int value = 1;
111 template <class Getter>
113 return Getter::template get<A>();
117 #include "dispatch
.h
"
119 template <class T> static int get() { return T::value; }
121 int v = dispatch<MyGetter>();
124 EXPECT_THAT(Recorded
.Roots
,
125 testing::ElementsAre(named("MyGetter"), named("v")));
128 class RecordPPTest
: public ::testing::Test
{
134 struct RecordAction
: public PreprocessOnlyAction
{
136 RecordAction(RecordedPP
&Out
) : Out(Out
) {}
138 void ExecuteAction() override
{
139 auto &PP
= getCompilerInstance().getPreprocessor();
140 PP
.addPPCallbacks(Out
.record(PP
));
141 PreprocessOnlyAction::ExecuteAction();
144 Inputs
.MakeAction
= [this] {
145 return std::make_unique
<RecordAction
>(Recorded
);
149 TestAST
build() { return TestAST(Inputs
); }
152 // Matches an Include with a particular spelling.
153 MATCHER_P(spelled
, S
, "") { return arg
.Spelled
== S
; }
155 TEST_F(RecordPPTest
, CapturesIncludes
) {
156 llvm::Annotations
MainFile(R
"cpp(
157 $H^#include "./header
.h
"
158 $M^#include <missing.h>
160 Inputs
.Code
= MainFile
.code();
161 Inputs
.ExtraFiles
["header.h"] = "";
162 Inputs
.ErrorOK
= true; // missing header
166 Recorded
.Includes
.all(),
167 testing::ElementsAre(spelled("./header.h"), spelled("missing.h")));
169 auto &H
= Recorded
.Includes
.all().front();
170 EXPECT_EQ(H
.Line
, 2u);
171 EXPECT_EQ(H
.HashLocation
,
172 AST
.sourceManager().getComposedLoc(
173 AST
.sourceManager().getMainFileID(), MainFile
.point("H")));
174 EXPECT_EQ(H
.Resolved
, AST
.fileManager().getFile("header.h").get());
175 EXPECT_FALSE(H
.Angled
);
177 auto &M
= Recorded
.Includes
.all().back();
178 EXPECT_EQ(M
.Line
, 3u);
179 EXPECT_EQ(M
.HashLocation
,
180 AST
.sourceManager().getComposedLoc(
181 AST
.sourceManager().getMainFileID(), MainFile
.point("M")));
182 EXPECT_EQ(M
.Resolved
, nullptr);
183 EXPECT_TRUE(M
.Angled
);
186 TEST_F(RecordPPTest
, CapturesMacroRefs
) {
187 llvm::Annotations
Header(R
"cpp(
190 // Refs, but not in main file.
194 llvm::Annotations
MainFile(R
"cpp(
195 #define EARLY X // not a ref, no definition
198 #define LATE2 ^X // a ref even if not expanded
201 int jeden = $exp^LATE; // a ref in LATE's expansion
203 #define IDENT(X) X // not a ref, shadowed
204 int eins = IDENT(^X);
207 // Not refs, rather a new macro with the same name.
211 Inputs
.Code
= MainFile
.code();
212 Inputs
.ExtraFiles
["header.h"] = Header
.code();
214 const auto &SM
= AST
.sourceManager();
216 SourceLocation Def
= SM
.getComposedLoc(
217 SM
.translateFile(AST
.fileManager().getFile("header.h").get()),
218 Header
.point("def"));
219 ASSERT_THAT(Recorded
.MacroReferences
, Not(IsEmpty()));
220 Symbol OrigX
= Recorded
.MacroReferences
.front().Target
;
221 EXPECT_EQ("X", OrigX
.macro().Name
->getName());
222 EXPECT_EQ(Def
, OrigX
.macro().Definition
);
224 std::vector
<unsigned> RefOffsets
;
225 std::vector
<unsigned> ExpOffsets
; // Expansion locs of refs in macro locs.
226 for (const auto &Ref
: Recorded
.MacroReferences
) {
227 if (Ref
.Target
== OrigX
) {
228 auto [FID
, Off
] = SM
.getDecomposedLoc(Ref
.RefLocation
);
229 if (FID
== SM
.getMainFileID()) {
230 RefOffsets
.push_back(Off
);
231 } else if (Ref
.RefLocation
.isMacroID() &&
232 SM
.isWrittenInMainFile(SM
.getExpansionLoc(Ref
.RefLocation
))) {
233 ExpOffsets
.push_back(
234 SM
.getDecomposedExpansionLoc(Ref
.RefLocation
).second
);
236 ADD_FAILURE() << Ref
.RefLocation
.printToString(SM
);
240 EXPECT_THAT(RefOffsets
, ElementsAreArray(MainFile
.points()));
241 EXPECT_THAT(ExpOffsets
, ElementsAreArray(MainFile
.points("exp")));
244 TEST_F(RecordPPTest
, CapturesConditionalMacroRefs
) {
245 llvm::Annotations
MainFile(R
"cpp(
266 Inputs
.Code
= MainFile
.code();
267 Inputs
.ExtraArgs
.push_back("-std=c++2b");
270 std::vector
<unsigned> RefOffsets
;
271 SourceManager
&SM
= AST
.sourceManager();
272 for (const auto &Ref
: Recorded
.MacroReferences
) {
273 auto [FID
, Off
] = SM
.getDecomposedLoc(Ref
.RefLocation
);
274 ASSERT_EQ(FID
, SM
.getMainFileID());
275 EXPECT_EQ(Ref
.RT
, RefType::Ambiguous
);
276 EXPECT_EQ("X", Ref
.Target
.macro().Name
->getName());
277 RefOffsets
.push_back(Off
);
279 EXPECT_THAT(RefOffsets
, ElementsAreArray(MainFile
.points()));
282 class PragmaIncludeTest
: public ::testing::Test
{
284 // We don't build an AST, we just run a preprocessor action!
288 PragmaIncludeTest() {
289 Inputs
.MakeAction
= [this] {
290 struct Hook
: public PreprocessOnlyAction
{
292 Hook(PragmaIncludes
*Out
) : Out(Out
) {}
293 bool BeginSourceFileAction(clang::CompilerInstance
&CI
) override
{
299 return std::make_unique
<Hook
>(&PI
);
303 TestAST
build() { return TestAST(Inputs
); }
305 void createEmptyFiles(llvm::ArrayRef
<StringRef
> FileNames
) {
306 for (llvm::StringRef File
: FileNames
)
307 Inputs
.ExtraFiles
[File
] = "";
311 TEST_F(PragmaIncludeTest
, IWYUKeep
) {
312 llvm::Annotations
MainFile(R
"cpp(
313 $keep1^#include "keep1
.h
" // IWYU pragma: keep
314 $keep2^#include "keep2
.h
" /* IWYU pragma: keep */
316 $export1^#include "export1
.h
" // IWYU pragma: export
317 $begin_exports^// IWYU pragma: begin_exports
318 $export2^#include "export2
.h
"
319 $export3^#include "export3
.h
"
320 $end_exports^// IWYU pragma: end_exports
322 $normal^#include "normal
.h
"
324 $begin_keep^// IWYU pragma: begin_keep
325 $keep3^#include "keep3
.h
"
326 $end_keep^// IWYU pragma: end_keep
328 // IWYU pragma: begin_keep
329 $keep4^#include "keep4
.h
"
330 // IWYU pragma: begin_keep
331 $keep5^#include "keep5
.h
"
332 // IWYU pragma: end_keep
333 $keep6^#include "keep6
.h
"
334 // IWYU pragma: end_keep
337 auto OffsetToLineNum
= [&MainFile
](size_t Offset
) {
338 int Count
= MainFile
.code().substr(0, Offset
).count('\n');
342 Inputs
.Code
= MainFile
.code();
343 createEmptyFiles({"keep1.h", "keep2.h", "keep3.h", "keep4.h", "keep5.h",
344 "keep6.h", "export1.h", "export2.h", "export3.h",
347 TestAST Processed
= build();
348 EXPECT_FALSE(PI
.shouldKeep(1));
351 EXPECT_TRUE(PI
.shouldKeep(OffsetToLineNum(MainFile
.point("keep1"))));
352 EXPECT_TRUE(PI
.shouldKeep(OffsetToLineNum(MainFile
.point("keep2"))));
354 EXPECT_FALSE(PI
.shouldKeep(
355 OffsetToLineNum(MainFile
.point("begin_keep")))); // no # directive
356 EXPECT_TRUE(PI
.shouldKeep(OffsetToLineNum(MainFile
.point("keep3"))));
357 EXPECT_FALSE(PI
.shouldKeep(
358 OffsetToLineNum(MainFile
.point("end_keep")))); // no # directive
360 EXPECT_TRUE(PI
.shouldKeep(OffsetToLineNum(MainFile
.point("keep4"))));
361 EXPECT_TRUE(PI
.shouldKeep(OffsetToLineNum(MainFile
.point("keep5"))));
362 EXPECT_TRUE(PI
.shouldKeep(OffsetToLineNum(MainFile
.point("keep6"))));
365 EXPECT_TRUE(PI
.shouldKeep(OffsetToLineNum(MainFile
.point("export1"))));
366 EXPECT_TRUE(PI
.shouldKeep(OffsetToLineNum(MainFile
.point("export2"))));
367 EXPECT_TRUE(PI
.shouldKeep(OffsetToLineNum(MainFile
.point("export3"))));
368 EXPECT_FALSE(PI
.shouldKeep(
369 OffsetToLineNum(MainFile
.point("begin_exports")))); // no # directive
370 EXPECT_FALSE(PI
.shouldKeep(
371 OffsetToLineNum(MainFile
.point("end_exports")))); // no # directive
373 EXPECT_FALSE(PI
.shouldKeep(OffsetToLineNum(MainFile
.point("normal"))));
376 TEST_F(PragmaIncludeTest
, IWYUPrivate
) {
380 Inputs
.ExtraFiles
["public.h"] = R
"cpp(
382 #include "private2
.h
"
384 Inputs
.ExtraFiles
["private.h"] = R
"cpp(
385 // IWYU pragma: private, include "public2
.h
"
387 Inputs
.ExtraFiles
["private2.h"] = R
"cpp(
388 // IWYU pragma: private
390 TestAST Processed
= build();
391 auto PrivateFE
= Processed
.fileManager().getFile("private.h");
393 EXPECT_TRUE(PI
.isPrivate(PrivateFE
.get()));
394 EXPECT_EQ(PI
.getPublic(PrivateFE
.get()), "\"public2.h\"");
396 auto PublicFE
= Processed
.fileManager().getFile("public.h");
398 EXPECT_EQ(PI
.getPublic(PublicFE
.get()), ""); // no mapping.
399 EXPECT_FALSE(PI
.isPrivate(PublicFE
.get()));
401 auto Private2FE
= Processed
.fileManager().getFile("private2.h");
403 EXPECT_TRUE(PI
.isPrivate(Private2FE
.get()));
406 TEST_F(PragmaIncludeTest
, IWYUExport
) {
407 Inputs
.Code
= R
"cpp(// Line 1
411 Inputs
.ExtraFiles
["export1.h"] = R
"cpp(
412 #include "private.h
" // IWYU pragma: export
414 Inputs
.ExtraFiles
["export2.h"] = R
"cpp(
417 Inputs
.ExtraFiles
["export3.h"] = R
"cpp(
418 #include "private.h
" // IWYU pragma: export
420 Inputs
.ExtraFiles
["private.h"] = "";
421 TestAST Processed
= build();
422 const auto &SM
= Processed
.sourceManager();
423 auto &FM
= Processed
.fileManager();
425 EXPECT_THAT(PI
.getExporters(FM
.getFile("private.h").get(), FM
),
426 testing::UnorderedElementsAre(FileNamed("export1.h"),
427 FileNamed("export3.h")));
429 EXPECT_TRUE(PI
.getExporters(FM
.getFile("export1.h").get(), FM
).empty());
430 EXPECT_TRUE(PI
.getExporters(FM
.getFile("export2.h").get(), FM
).empty());
431 EXPECT_TRUE(PI
.getExporters(FM
.getFile("export3.h").get(), FM
).empty());
433 PI
.getExporters(SM
.getFileEntryForID(SM
.getMainFileID()), FM
).empty());
436 TEST_F(PragmaIncludeTest
, IWYUExportForStandardHeaders
) {
440 Inputs
.ExtraFiles
["export.h"] = R
"cpp(
441 #include <string> // IWYU pragma: export
443 Inputs
.ExtraFiles
["string"] = "";
444 Inputs
.ExtraArgs
= {"-isystem."};
445 TestAST Processed
= build();
446 auto &FM
= Processed
.fileManager();
447 EXPECT_THAT(PI
.getExporters(*tooling::stdlib::Header::named("<string>"), FM
),
448 testing::UnorderedElementsAre(FileNamed("export.h")));
451 TEST_F(PragmaIncludeTest
, IWYUExportBlock
) {
452 Inputs
.Code
= R
"cpp(// Line 1
455 Inputs
.ExtraFiles
["normal.h"] = R
"cpp(
458 // IWYU pragma: begin_exports
460 #include "private1
.h
"
461 // IWYU pragma: end_exports
463 Inputs
.ExtraFiles
["export1.h"] = R
"cpp(
464 // IWYU pragma: begin_exports
465 #include "private1
.h
"
466 #include "private2
.h
"
467 // IWYU pragma: end_exports
470 #include "private3
.h
" // IWYU pragma: export
473 {"private1.h", "private2.h", "private3.h", "foo.h", "bar.h"});
474 TestAST Processed
= build();
475 auto &FM
= Processed
.fileManager();
477 EXPECT_THAT(PI
.getExporters(FM
.getFile("private1.h").get(), FM
),
478 testing::UnorderedElementsAre(FileNamed("export1.h"),
479 FileNamed("normal.h")));
480 EXPECT_THAT(PI
.getExporters(FM
.getFile("private2.h").get(), FM
),
481 testing::UnorderedElementsAre(FileNamed("export1.h")));
482 EXPECT_THAT(PI
.getExporters(FM
.getFile("private3.h").get(), FM
),
483 testing::UnorderedElementsAre(FileNamed("export1.h")));
485 EXPECT_TRUE(PI
.getExporters(FM
.getFile("foo.h").get(), FM
).empty());
486 EXPECT_TRUE(PI
.getExporters(FM
.getFile("bar.h").get(), FM
).empty());
489 TEST_F(PragmaIncludeTest
, SelfContained
) {
493 #include "unguarded
.h
"
495 Inputs
.ExtraFiles
["guarded.h"] = R
"cpp(
498 Inputs
.ExtraFiles
["unguarded.h"] = "";
499 TestAST Processed
= build();
500 auto &FM
= Processed
.fileManager();
501 EXPECT_TRUE(PI
.isSelfContained(FM
.getFile("guarded.h").get()));
502 EXPECT_FALSE(PI
.isSelfContained(FM
.getFile("unguarded.h").get()));
506 } // namespace clang::include_cleaner