1 //===--- IncludeCleanerTests.cpp --------------------------------*- C++ -*-===//
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 "Annotations.h"
10 #include "Diagnostics.h"
11 #include "IncludeCleaner.h"
12 #include "ParsedAST.h"
13 #include "SourceCode.h"
16 #include "clang-include-cleaner/Analysis.h"
17 #include "clang-include-cleaner/Types.h"
18 #include "support/Context.h"
19 #include "clang/AST/DeclBase.h"
20 #include "clang/Basic/SourceManager.h"
21 #include "clang/Tooling/Syntax/Tokens.h"
22 #include "llvm/ADT/ScopeExit.h"
23 #include "llvm/ADT/StringRef.h"
24 #include "llvm/Support/Casting.h"
25 #include "llvm/Support/Error.h"
26 #include "llvm/Support/ScopedPrinter.h"
27 #include "llvm/Testing/Support/SupportHelpers.h"
28 #include "gmock/gmock.h"
29 #include "gtest/gtest.h"
40 using ::testing::AllOf
;
41 using ::testing::ElementsAre
;
42 using ::testing::IsEmpty
;
43 using ::testing::Matcher
;
44 using ::testing::Pointee
;
45 using ::testing::UnorderedElementsAre
;
48 withFix(std::vector
<::testing::Matcher
<Fix
>> FixMatcheres
) {
49 return Field(&Diag::Fixes
, testing::UnorderedElementsAreArray(FixMatcheres
));
52 MATCHER_P2(Diag
, Range
, Message
,
53 "Diag at " + llvm::to_string(Range
) + " = [" + Message
+ "]") {
54 return arg
.Range
== Range
&& arg
.Message
== Message
;
57 MATCHER_P3(Fix
, Range
, Replacement
, Message
,
58 "Fix " + llvm::to_string(Range
) + " => " +
59 ::testing::PrintToString(Replacement
) + " = [" + Message
+ "]") {
60 return arg
.Message
== Message
&& arg
.Edits
.size() == 1 &&
61 arg
.Edits
[0].range
== Range
&& arg
.Edits
[0].newText
== Replacement
;
63 MATCHER_P(FixMessage
, Message
, "") { return arg
.Message
== Message
; }
65 std::string
guard(llvm::StringRef Code
) {
66 return "#pragma once\n" + Code
.str();
69 MATCHER_P(writtenInclusion
, Written
, "") {
70 if (arg
.Written
!= Written
)
71 *result_listener
<< arg
.Written
;
72 return arg
.Written
== Written
;
75 TEST(IncludeCleaner
, StdlibUnused
) {
76 setIncludeCleanerAnalyzesStdlib(true);
78 llvm::make_scope_exit([] { setIncludeCleanerAnalyzesStdlib(false); });
80 auto TU
= TestTU::withCode(R
"cpp(
85 // Layout of std library impl is not relevant.
86 TU
.AdditionalFiles
["bits"] = R
"cpp(
89 template <typename> class list {};
90 template <typename> class queue {};
93 TU
.AdditionalFiles
["list"] = "#include <bits>";
94 TU
.AdditionalFiles
["queue"] = "#include <bits>";
95 TU
.ExtraArgs
= {"-isystem", testRoot()};
96 auto AST
= TU
.build();
97 IncludeCleanerFindings Findings
= computeIncludeCleanerFindings(AST
);
98 EXPECT_THAT(Findings
.UnusedIncludes
,
99 ElementsAre(Pointee(writtenInclusion("<queue>"))));
102 TEST(IncludeCleaner
, GetUnusedHeaders
) {
103 llvm::StringLiteral MainFile
= R
"cpp(
107 #include "dir
/unused
.h
"
108 #include "unguarded
.h
"
110 #include <system_header.h>
116 // Build expected ast with symbols coming from headers.
118 TU
.Filename
= "foo.cpp";
119 TU
.AdditionalFiles
["foo.h"] = guard("void foo();");
120 TU
.AdditionalFiles
["a.h"] = guard("void a();");
121 TU
.AdditionalFiles
["b.h"] = guard("void b();");
122 TU
.AdditionalFiles
["dir/c.h"] = guard("void c();");
123 TU
.AdditionalFiles
["unused.h"] = guard("void unused();");
124 TU
.AdditionalFiles
["dir/unused.h"] = guard("void dirUnused();");
125 TU
.AdditionalFiles
["system/system_header.h"] = guard("");
126 TU
.AdditionalFiles
["unguarded.h"] = "";
127 TU
.ExtraArgs
.push_back("-I" + testPath("dir"));
128 TU
.ExtraArgs
.push_back("-isystem" + testPath("system"));
129 TU
.Code
= MainFile
.str();
130 ParsedAST AST
= TU
.build();
131 IncludeCleanerFindings Findings
= computeIncludeCleanerFindings(AST
);
133 Findings
.UnusedIncludes
,
134 UnorderedElementsAre(Pointee(writtenInclusion("\"unused.h\"")),
135 Pointee(writtenInclusion("\"dir/unused.h\""))));
138 TEST(IncludeCleaner
, ComputeMissingHeaders
) {
139 Annotations
MainFile(R
"cpp(
146 TU
.Filename
= "foo.cpp";
147 TU
.AdditionalFiles
["a.h"] = guard("#include \"b.h\"");
148 TU
.AdditionalFiles
["b.h"] = guard("void b();");
150 TU
.Code
= MainFile
.code();
151 ParsedAST AST
= TU
.build();
153 IncludeCleanerFindings Findings
= computeIncludeCleanerFindings(AST
);
154 const SourceManager
&SM
= AST
.getSourceManager();
155 const NamedDecl
*BDecl
= nullptr;
156 for (Decl
*D
: AST
.getASTContext().getTranslationUnitDecl()->decls()) {
157 const NamedDecl
*CandidateDecl
= llvm::dyn_cast
<NamedDecl
>(D
);
158 std::string Name
= CandidateDecl
->getQualifiedNameAsString();
161 BDecl
= CandidateDecl
;
164 include_cleaner::Symbol B
{*BDecl
};
165 auto Range
= MainFile
.range("b");
166 size_t Start
= llvm::cantFail(positionToOffset(MainFile
.code(), Range
.start
));
167 size_t End
= llvm::cantFail(positionToOffset(MainFile
.code(), Range
.end
));
168 syntax::FileRange BRange
{SM
.getMainFileID(), static_cast<unsigned int>(Start
),
169 static_cast<unsigned int>(End
)};
170 include_cleaner::Header Header
{*SM
.getFileManager().getFile("b.h")};
171 MissingIncludeDiagInfo BInfo
{B
, BRange
, {Header
}};
172 EXPECT_THAT(Findings
.MissingIncludes
, ElementsAre(BInfo
));
175 TEST(IncludeCleaner
, GenerateMissingHeaderDiags
) {
176 Annotations
MainFile(R
"cpp(
179 $insert_b[[]]#include "baz
.h
"
181 $insert_d[[]]$insert_foo[[]]#include "fuzz
.h
"
183 $insert_foobar[[]]#include <e.h>
184 $insert_f[[]]$insert_vector[[]]
186 #define DEF(X) const Foo *X;
187 #define BAZ(X) const X x
189 // No missing include insertion for ambiguous macro refs.
200 // this should not be diagnosed, because it's ignored in the config
205 std::$vector[[vector]] v;
207 int var = $FOO[[FOO]];
217 TU
.Filename
= "main.cpp";
218 TU
.AdditionalFiles
["a.h"] = guard("#include \"b.h\"");
219 TU
.AdditionalFiles
["b.h"] = guard("void b();");
221 TU
.AdditionalFiles
["dir/c.h"] = guard("#include \"d.h\"");
222 TU
.AdditionalFiles
["dir/d.h"] =
223 guard("namespace ns { struct Bar { void d(); }; }");
225 TU
.AdditionalFiles
["system/e.h"] = guard("#include <f.h>");
226 TU
.AdditionalFiles
["system/f.h"] = guard("void f();");
227 TU
.ExtraArgs
.push_back("-isystem" + testPath("system"));
229 TU
.AdditionalFiles
["fuzz.h"] = guard("#include \"buzz.h\"");
230 TU
.AdditionalFiles
["buzz.h"] = guard("void buzz();");
232 TU
.AdditionalFiles
["baz.h"] = guard("#include \"private.h\"");
233 TU
.AdditionalFiles
["private.h"] = guard(R
"cpp(
234 // IWYU pragma: private, include "public.h
"
237 TU
.AdditionalFiles
["header.h"] = guard(R
"cpp(
238 namespace std { class vector {}; }
241 TU
.AdditionalFiles
["all.h"] = guard("#include \"foo.h\"");
242 TU
.AdditionalFiles
["foo.h"] = guard(R
"cpp(
243 #define BAR(x) Foo *x
248 TU
.Code
= MainFile
.code();
249 ParsedAST AST
= TU
.build();
251 auto Findings
= computeIncludeCleanerFindings(AST
);
252 Findings
.UnusedIncludes
.clear();
253 std::vector
<clangd::Diag
> Diags
= issueIncludeCleanerDiagnostics(
254 AST
, TU
.Code
, Findings
,
255 {[](llvm::StringRef Header
) { return Header
.ends_with("buzz.h"); }});
258 UnorderedElementsAre(
259 AllOf(Diag(MainFile
.range("b"),
260 "No header providing \"b\" is directly included"),
261 withFix({Fix(MainFile
.range("insert_b"), "#include \"b.h\"\n",
263 FixMessage("add all missing includes")})),
264 AllOf(Diag(MainFile
.range("bar"),
265 "No header providing \"ns::Bar\" is directly included"),
266 withFix({Fix(MainFile
.range("insert_d"),
267 "#include \"dir/d.h\"\n", "#include \"dir/d.h\""),
268 FixMessage("add all missing includes")})),
269 AllOf(Diag(MainFile
.range("f"),
270 "No header providing \"f\" is directly included"),
271 withFix({Fix(MainFile
.range("insert_f"), "#include <f.h>\n",
273 FixMessage("add all missing includes")})),
275 Diag(MainFile
.range("foobar"),
276 "No header providing \"foobar\" is directly included"),
277 withFix({Fix(MainFile
.range("insert_foobar"),
278 "#include \"public.h\"\n", "#include \"public.h\""),
279 FixMessage("add all missing includes")})),
281 Diag(MainFile
.range("vector"),
282 "No header providing \"std::vector\" is directly included"),
284 Fix(MainFile
.range("insert_vector"), "#include <vector>\n",
285 "#include <vector>"),
286 FixMessage("add all missing includes"),
288 AllOf(Diag(MainFile
.range("FOO"),
289 "No header providing \"FOO\" is directly included"),
290 withFix({Fix(MainFile
.range("insert_foo"),
291 "#include \"foo.h\"\n", "#include \"foo.h\""),
292 FixMessage("add all missing includes")})),
293 AllOf(Diag(MainFile
.range("DEF"),
294 "No header providing \"Foo\" is directly included"),
295 withFix({Fix(MainFile
.range("insert_foo"),
296 "#include \"foo.h\"\n", "#include \"foo.h\""),
297 FixMessage("add all missing includes")})),
298 AllOf(Diag(MainFile
.range("BAR"),
299 "No header providing \"BAR\" is directly included"),
300 withFix({Fix(MainFile
.range("insert_foo"),
301 "#include \"foo.h\"\n", "#include \"foo.h\""),
302 FixMessage("add all missing includes")})),
303 AllOf(Diag(MainFile
.range("Foo"),
304 "No header providing \"Foo\" is directly included"),
305 withFix({Fix(MainFile
.range("insert_foo"),
306 "#include \"foo.h\"\n", "#include \"foo.h\""),
307 FixMessage("add all missing includes")}))));
310 TEST(IncludeCleaner
, IWYUPragmas
) {
313 #include "behind_keep
.h
" // IWYU pragma: keep
314 #include "exported
.h
" // IWYU pragma: export
317 void bar() { foo(); }
319 TU
.AdditionalFiles
["behind_keep.h"] = guard("");
320 TU
.AdditionalFiles
["exported.h"] = guard("");
321 TU
.AdditionalFiles
["public.h"] = guard("#include \"private.h\"");
322 TU
.AdditionalFiles
["private.h"] = guard(R
"cpp(
323 // IWYU pragma: private, include "public.h
"
326 ParsedAST AST
= TU
.build();
327 IncludeCleanerFindings Findings
= computeIncludeCleanerFindings(AST
);
328 EXPECT_THAT(Findings
.UnusedIncludes
, IsEmpty());
331 TEST(IncludeCleaner
, IWYUPragmaExport
) {
336 TU
.AdditionalFiles
["foo.h"] = R
"cpp(
340 #include "bar
.h
" // IWYU pragma: export
344 TU
.AdditionalFiles
["bar.h"] = guard(R
"cpp(
347 ParsedAST AST
= TU
.build();
349 IncludeCleanerFindings Findings
= computeIncludeCleanerFindings(AST
);
350 EXPECT_THAT(Findings
.UnusedIncludes
,
351 ElementsAre(Pointee(writtenInclusion("\"foo.h\""))));
354 TEST(IncludeCleaner
, NoDiagsForObjC
) {
361 TU
.AdditionalFiles
["foo.h"] = R
"cpp(
367 TU
.ExtraArgs
.emplace_back("-xobjective-c");
369 ParsedAST AST
= TU
.build();
370 IncludeCleanerFindings Findings
= computeIncludeCleanerFindings(AST
);
371 EXPECT_THAT(Findings
.MissingIncludes
, IsEmpty());
372 EXPECT_THAT(Findings
.UnusedIncludes
, IsEmpty());
375 TEST(IncludeCleaner
, UmbrellaUsesPrivate
) {
380 TU
.AdditionalFiles
["private.h"] = guard(R
"cpp(
381 // IWYU pragma: private, include "public.h
"
384 TU
.Filename
= "public.h";
385 ParsedAST AST
= TU
.build();
386 IncludeCleanerFindings Findings
= computeIncludeCleanerFindings(AST
);
387 EXPECT_THAT(Findings
.UnusedIncludes
, IsEmpty());
390 TEST(IncludeCleaner
, MacroExpandedThroughIncludes
) {
391 Annotations
MainFile(R
"cpp(
393 #define FOO(X) const Foo *X
395 #include [["expander
.inc
"]]
400 TU
.AdditionalFiles
["expander.inc"] = guard("FOO(f1);FOO(f2);");
401 TU
.AdditionalFiles
["foo.h"] = guard("struct Foo {};");
402 TU
.AdditionalFiles
["all.h"] = guard("#include \"foo.h\"");
404 TU
.Code
= MainFile
.code();
405 ParsedAST AST
= TU
.build();
407 auto Findings
= computeIncludeCleanerFindings(AST
).MissingIncludes
;
408 EXPECT_THAT(Findings
, testing::SizeIs(1));
409 auto RefRange
= Findings
.front().SymRefRange
;
410 auto &SM
= AST
.getSourceManager();
411 EXPECT_EQ(RefRange
.file(), SM
.getMainFileID());
412 // FIXME: Point at the spelling location, rather than the include.
413 EXPECT_EQ(halfOpenToRange(SM
, RefRange
.toCharRange(SM
)), MainFile
.range());
416 TEST(IncludeCleaner
, MissingIncludesAreUnique
) {
417 Annotations
MainFile(R
"cpp(
423 TU
.AdditionalFiles
["foo.h"] = guard("struct Foo {};");
424 TU
.AdditionalFiles
["all.h"] = guard(R
"cpp(
426 #define FOO(X) X y; X z
429 TU
.Code
= MainFile
.code();
430 ParsedAST AST
= TU
.build();
432 auto Findings
= computeIncludeCleanerFindings(AST
).MissingIncludes
;
433 EXPECT_THAT(Findings
, testing::SizeIs(1));
434 auto RefRange
= Findings
.front().SymRefRange
;
435 auto &SM
= AST
.getSourceManager();
436 EXPECT_EQ(RefRange
.file(), SM
.getMainFileID());
437 EXPECT_EQ(halfOpenToRange(SM
, RefRange
.toCharRange(SM
)), MainFile
.range());
440 TEST(IncludeCleaner
, NoCrash
) {
442 Annotations
MainCode(R
"cpp(
448 TU
.Code
= MainCode
.code();
449 TU
.AdditionalFiles
["foo.h"] =
450 guard("int operator\"\"s(unsigned long long) { return 0; }");
451 TU
.AdditionalFiles
["all.h"] = guard("#include \"foo.h\"");
452 ParsedAST AST
= TU
.build();
453 const auto &MissingIncludes
=
454 computeIncludeCleanerFindings(AST
).MissingIncludes
;
455 EXPECT_THAT(MissingIncludes
, testing::SizeIs(1));
456 auto &SM
= AST
.getSourceManager();
458 halfOpenToRange(SM
, MissingIncludes
.front().SymRefRange
.toCharRange(SM
)),
462 TEST(IncludeCleaner
, FirstMatchedProvider
) {
465 const std::vector
<include_cleaner::Header
> Providers
;
466 const std::optional
<include_cleaner::Header
> ExpectedProvider
;
472 {include_cleaner::Header
{"bar.h"}, include_cleaner::Header
{"foo.h"}},
473 include_cleaner::Header
{"bar.h"}},
478 {include_cleaner::Header
{"foo.h"}, include_cleaner::Header
{"bar.h"}},
479 include_cleaner::Header
{"foo.h"}},
480 {"#include \"bar.h\"",
481 {include_cleaner::Header
{"bar.h"}},
482 include_cleaner::Header
{"bar.h"}},
483 {"#include \"bar.h\"", {include_cleaner::Header
{"foo.h"}}, std::nullopt
},
484 {"#include \"bar.h\"", {}, std::nullopt
}};
485 for (const auto &Case
: Cases
) {
486 Annotations Code
{Case
.Code
};
487 SCOPED_TRACE(Code
.code());
490 TU
.Code
= Code
.code();
491 TU
.AdditionalFiles
["bar.h"] = "";
492 TU
.AdditionalFiles
["foo.h"] = "";
494 auto AST
= TU
.build();
495 std::optional
<include_cleaner::Header
> MatchedProvider
=
496 firstMatchedProvider(
497 convertIncludes(AST
.getSourceManager(),
498 AST
.getIncludeStructure().MainFileIncludes
),
500 EXPECT_EQ(MatchedProvider
, Case
.ExpectedProvider
);
504 TEST(IncludeCleaner
, BatchFix
) {
506 TU
.Filename
= "main.cpp";
507 TU
.AdditionalFiles
["foo.h"] = guard("class Foo;");
508 TU
.AdditionalFiles
["bar.h"] = guard("class Bar;");
509 TU
.AdditionalFiles
["all.h"] = guard(R
"cpp(
519 auto AST
= TU
.build();
521 issueIncludeCleanerDiagnostics(AST
, TU
.Code
,
522 computeIncludeCleanerFindings(AST
)),
523 UnorderedElementsAre(withFix({FixMessage("#include \"foo.h\""),
524 FixMessage("fix all includes")}),
525 withFix({FixMessage("remove #include directive"),
526 FixMessage("fix all includes")})));
536 issueIncludeCleanerDiagnostics(AST
, TU
.Code
,
537 computeIncludeCleanerFindings(AST
)),
538 UnorderedElementsAre(withFix({FixMessage("#include \"foo.h\""),
539 FixMessage("fix all includes")}),
540 withFix({FixMessage("remove #include directive"),
541 FixMessage("remove all unused includes"),
542 FixMessage("fix all includes")}),
543 withFix({FixMessage("remove #include directive"),
544 FixMessage("remove all unused includes"),
545 FixMessage("fix all includes")})));
555 issueIncludeCleanerDiagnostics(AST
, TU
.Code
,
556 computeIncludeCleanerFindings(AST
)),
557 UnorderedElementsAre(withFix({FixMessage("#include \"foo.h\""),
558 FixMessage("add all missing includes"),
559 FixMessage("fix all includes")}),
560 withFix({FixMessage("#include \"bar.h\""),
561 FixMessage("add all missing includes"),
562 FixMessage("fix all includes")}),
563 withFix({FixMessage("remove #include directive"),
564 FixMessage("fix all includes")})));
568 } // namespace clangd