1 | //===--- IncludeCleanerTests.cpp --------------------------------*- C++ -*-===// |
2 | // |
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 |
6 | // |
7 | //===----------------------------------------------------------------------===// |
8 | |
9 | #include "Annotations.h" |
10 | #include "Diagnostics.h" |
11 | #include "IncludeCleaner.h" |
12 | #include "ParsedAST.h" |
13 | #include "SourceCode.h" |
14 | #include "TestFS.h" |
15 | #include "TestTU.h" |
16 | #include "clang-include-cleaner/Analysis.h" |
17 | #include "clang-include-cleaner/Types.h" |
18 | #include "clang/AST/DeclBase.h" |
19 | #include "clang/Basic/SourceManager.h" |
20 | #include "clang/Tooling/Syntax/Tokens.h" |
21 | #include "llvm/ADT/ArrayRef.h" |
22 | #include "llvm/ADT/ScopeExit.h" |
23 | #include "llvm/ADT/StringMap.h" |
24 | #include "llvm/ADT/StringRef.h" |
25 | #include "llvm/Support/Casting.h" |
26 | #include "llvm/Support/Error.h" |
27 | #include "llvm/Support/ScopedPrinter.h" |
28 | #include "gmock/gmock.h" |
29 | #include "gtest/gtest.h" |
30 | #include <cstddef> |
31 | #include <optional> |
32 | #include <string> |
33 | #include <vector> |
34 | |
35 | namespace clang { |
36 | namespace clangd { |
37 | namespace { |
38 | |
39 | using ::testing::AllOf; |
40 | using ::testing::ElementsAre; |
41 | using ::testing::IsEmpty; |
42 | using ::testing::Matcher; |
43 | using ::testing::Pointee; |
44 | using ::testing::UnorderedElementsAre; |
45 | |
46 | Matcher<const Diag &> |
47 | withFix(std::vector<::testing::Matcher<Fix>> FixMatcheres) { |
48 | return Field(field: &Diag::Fixes, matcher: testing::UnorderedElementsAreArray(container: FixMatcheres)); |
49 | } |
50 | |
51 | MATCHER_P2(Diag, Range, Message, |
52 | "Diag at " + llvm::to_string(Range) + " = [" + Message + "]" ) { |
53 | return arg.Range == Range && arg.Message == Message; |
54 | } |
55 | |
56 | MATCHER_P3(Fix, Range, Replacement, Message, |
57 | "Fix " + llvm::to_string(Range) + " => " + |
58 | ::testing::PrintToString(Replacement) + " = [" + Message + "]" ) { |
59 | return arg.Message == Message && arg.Edits.size() == 1 && |
60 | arg.Edits[0].range == Range && arg.Edits[0].newText == Replacement; |
61 | } |
62 | MATCHER_P(FixMessage, Message, "" ) { return arg.Message == Message; } |
63 | |
64 | std::string guard(llvm::StringRef Code) { |
65 | return "#pragma once\n" + Code.str(); |
66 | } |
67 | |
68 | MATCHER_P(writtenInclusion, Written, "" ) { |
69 | if (arg.Written != Written) |
70 | *result_listener << arg.Written; |
71 | return arg.Written == Written; |
72 | } |
73 | |
74 | TEST(IncludeCleaner, StdlibUnused) { |
75 | auto TU = TestTU::withCode(Code: R"cpp( |
76 | #include <list> |
77 | #include <queue> |
78 | #include <vector> // IWYU pragma: keep |
79 | #include <string> // IWYU pragma: export |
80 | std::list<int> x; |
81 | )cpp" ); |
82 | // Layout of std library impl is not relevant. |
83 | TU.AdditionalFiles["bits" ] = R"cpp( |
84 | #pragma once |
85 | namespace std { |
86 | template <typename> class list {}; |
87 | template <typename> class queue {}; |
88 | template <typename> class vector {}; |
89 | } |
90 | )cpp" ; |
91 | TU.AdditionalFiles["list" ] = guard(Code: "#include <bits>" ); |
92 | TU.AdditionalFiles["queue" ] = guard(Code: "#include <bits>" ); |
93 | TU.AdditionalFiles["vector" ] = guard(Code: "#include <bits>" ); |
94 | TU.AdditionalFiles["string" ] = guard(Code: "#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>" )))); |
100 | } |
101 | |
102 | TEST(IncludeCleaner, GetUnusedHeaders) { |
103 | llvm::StringLiteral MainFile = R"cpp( |
104 | #include "a.h" |
105 | #include "b.h" |
106 | #include "dir/c.h" |
107 | #include "dir/unused.h" |
108 | #include "unguarded.h" |
109 | #include "unused.h" |
110 | #include <system_header.h> |
111 | #include <non_system_angled_header.h> |
112 | void foo() { |
113 | a(); |
114 | b(); |
115 | c(); |
116 | })cpp" ; |
117 | // Build expected ast with symbols coming from headers. |
118 | TestTU TU; |
119 | TU.Filename = "foo.cpp" ; |
120 | TU.AdditionalFiles["foo.h" ] = guard(Code: "void foo();" ); |
121 | TU.AdditionalFiles["a.h" ] = guard(Code: "void a();" ); |
122 | TU.AdditionalFiles["b.h" ] = guard(Code: "void b();" ); |
123 | TU.AdditionalFiles["dir/c.h" ] = guard(Code: "void c();" ); |
124 | TU.AdditionalFiles["unused.h" ] = guard(Code: "void unused();" ); |
125 | TU.AdditionalFiles["dir/unused.h" ] = guard(Code: "void dirUnused();" ); |
126 | TU.AdditionalFiles["dir/non_system_angled_header.h" ] = guard(Code: "" ); |
127 | TU.AdditionalFiles["system/system_header.h" ] = guard(Code: "" ); |
128 | TU.AdditionalFiles["unguarded.h" ] = "" ; |
129 | TU.ExtraArgs.push_back(x: "-I" + testPath(File: "dir" )); |
130 | TU.ExtraArgs.push_back(x: "-isystem" + testPath(File: "system" )); |
131 | TU.Code = MainFile.str(); |
132 | ParsedAST AST = TU.build(); |
133 | IncludeCleanerFindings Findings = computeIncludeCleanerFindings(AST); |
134 | EXPECT_THAT( |
135 | Findings.UnusedIncludes, |
136 | UnorderedElementsAre(Pointee(writtenInclusion("\"unused.h\"" )), |
137 | Pointee(writtenInclusion("\"dir/unused.h\"" )))); |
138 | } |
139 | |
140 | TEST(IncludeCleaner, IgnoredAngledHeaders) { |
141 | // Currently the default behavior is to ignore unused angled includes |
142 | auto TU = TestTU::withCode(Code: R"cpp( |
143 | #include <system_header.h> |
144 | #include <system_unused.h> |
145 | #include <non_system_angled_unused.h> |
146 | SystemClass x; |
147 | )cpp" ); |
148 | TU.AdditionalFiles["system/system_header.h" ] = guard(Code: "class SystemClass {};" ); |
149 | TU.AdditionalFiles["system/system_unused.h" ] = guard(Code: "" ); |
150 | TU.AdditionalFiles["dir/non_system_angled_unused.h" ] = guard(Code: "" ); |
151 | TU.ExtraArgs = { |
152 | "-isystem" + testPath(File: "system" ), |
153 | "-I" + testPath(File: "dir" ), |
154 | }; |
155 | auto AST = TU.build(); |
156 | IncludeCleanerFindings Findings = computeIncludeCleanerFindings(AST); |
157 | EXPECT_THAT(Findings.UnusedIncludes, IsEmpty()); |
158 | } |
159 | |
160 | TEST(IncludeCleaner, UnusedAngledHeaders) { |
161 | auto TU = TestTU::withCode(Code: R"cpp( |
162 | #include <system_header.h> |
163 | #include <system_unused.h> |
164 | #include <non_system_angled_unused.h> |
165 | SystemClass x; |
166 | )cpp" ); |
167 | TU.AdditionalFiles["system/system_header.h" ] = guard(Code: "class SystemClass {};" ); |
168 | TU.AdditionalFiles["system/system_unused.h" ] = guard(Code: "" ); |
169 | TU.AdditionalFiles["dir/non_system_angled_unused.h" ] = guard(Code: "" ); |
170 | TU.ExtraArgs = { |
171 | "-isystem" + testPath(File: "system" ), |
172 | "-I" + testPath(File: "dir" ), |
173 | }; |
174 | auto AST = TU.build(); |
175 | IncludeCleanerFindings Findings = computeIncludeCleanerFindings(AST, AnalyzeAngledIncludes: true); |
176 | EXPECT_THAT(Findings.UnusedIncludes, |
177 | UnorderedElementsAre( |
178 | Pointee(writtenInclusion("<system_unused.h>" )), |
179 | Pointee(writtenInclusion("<non_system_angled_unused.h>" )))); |
180 | } |
181 | |
182 | TEST(IncludeCleaner, ComputeMissingHeaders) { |
183 | Annotations MainFile(R"cpp( |
184 | #include "a.h" |
185 | |
186 | void foo() { |
187 | $b[[b]](); |
188 | })cpp" ); |
189 | TestTU TU; |
190 | TU.Filename = "foo.cpp" ; |
191 | TU.AdditionalFiles["a.h" ] = guard(Code: "#include \"b.h\"" ); |
192 | TU.AdditionalFiles["b.h" ] = guard(Code: "void b();" ); |
193 | |
194 | TU.Code = MainFile.code(); |
195 | ParsedAST AST = TU.build(); |
196 | |
197 | IncludeCleanerFindings Findings = computeIncludeCleanerFindings(AST); |
198 | const SourceManager &SM = AST.getSourceManager(); |
199 | const NamedDecl *BDecl = nullptr; |
200 | for (Decl *D : AST.getASTContext().getTranslationUnitDecl()->decls()) { |
201 | const NamedDecl *CandidateDecl = llvm::dyn_cast<NamedDecl>(D); |
202 | std::string Name = CandidateDecl->getQualifiedNameAsString(); |
203 | if (Name != "b" ) |
204 | continue; |
205 | BDecl = CandidateDecl; |
206 | } |
207 | ASSERT_TRUE(BDecl); |
208 | include_cleaner::Symbol B{*BDecl}; |
209 | auto Range = MainFile.range(Name: "b" ); |
210 | size_t Start = llvm::cantFail(ValOrErr: positionToOffset(Code: MainFile.code(), P: Range.start)); |
211 | size_t End = llvm::cantFail(ValOrErr: positionToOffset(Code: MainFile.code(), P: Range.end)); |
212 | syntax::FileRange BRange{SM.getMainFileID(), static_cast<unsigned int>(Start), |
213 | static_cast<unsigned int>(End)}; |
214 | include_cleaner::Header { |
215 | *SM.getFileManager().getOptionalFileRef(Filename: "b.h" )}; |
216 | MissingIncludeDiagInfo BInfo{.Symbol: B, .SymRefRange: BRange, .Providers: {Header}}; |
217 | EXPECT_THAT(Findings.MissingIncludes, ElementsAre(BInfo)); |
218 | } |
219 | |
220 | TEST(IncludeCleaner, GenerateMissingHeaderDiags) { |
221 | Annotations MainFile(R"cpp( |
222 | #include "a.h" |
223 | #include "angled_wrapper.h" |
224 | #include "all.h" |
225 | $insert_b[[]]#include "baz.h" |
226 | #include "dir/c.h" |
227 | $insert_d[[]]$insert_foo[[]]#include "fuzz.h" |
228 | #include "header.h" |
229 | $insert_foobar[[]]$insert_quoted[[]]$insert_quoted2[[]]#include "quoted_wrapper.h" |
230 | $insert_angled[[]]#include <e.h> |
231 | $insert_f[[]]#include <quoted2_wrapper.h> |
232 | $insert_vector[[]] |
233 | |
234 | #define DEF(X) const Foo *X; |
235 | #define BAZ(X) const X x |
236 | |
237 | // No missing include insertion for ambiguous macro refs. |
238 | #if defined(FOO) |
239 | #endif |
240 | |
241 | void foo() { |
242 | $b[[b]](); |
243 | $angled[[angled]](); |
244 | $quoted[[quoted]](); |
245 | $quoted2[[quoted2]](); |
246 | |
247 | ns::$bar[[Bar]] bar; |
248 | bar.d(); |
249 | $f[[f]](); |
250 | |
251 | // this should not be diagnosed, because it's ignored in the config |
252 | buzz(); |
253 | |
254 | $foobar[[foobar]](); |
255 | |
256 | std::$vector[[vector]] v; |
257 | |
258 | int var = $FOO[[FOO]]; |
259 | |
260 | $DEF[[DEF]](a); |
261 | |
262 | $BAR[[BAR]](b); |
263 | |
264 | BAZ($Foo[[Foo]]); |
265 | })cpp" ); |
266 | |
267 | TestTU TU; |
268 | TU.Filename = "main.cpp" ; |
269 | TU.AdditionalFiles["a.h" ] = guard(Code: "#include \"b.h\"" ); |
270 | TU.AdditionalFiles["b.h" ] = guard(Code: "void b();" ); |
271 | |
272 | TU.AdditionalFiles["angled_wrapper.h" ] = guard(Code: "#include <angled.h>" ); |
273 | TU.AdditionalFiles["angled.h" ] = guard(Code: "void angled();" ); |
274 | TU.ExtraArgs.push_back(x: "-I" + testPath(File: "." )); |
275 | |
276 | TU.AdditionalFiles["quoted_wrapper.h" ] = guard(Code: "#include \"quoted.h\"" ); |
277 | TU.AdditionalFiles["quoted.h" ] = guard(Code: "void quoted();" ); |
278 | |
279 | TU.AdditionalFiles["dir/c.h" ] = guard(Code: "#include \"d.h\"" ); |
280 | TU.AdditionalFiles["dir/d.h" ] = |
281 | guard(Code: "namespace ns { struct Bar { void d(); }; }" ); |
282 | |
283 | TU.AdditionalFiles["system/e.h" ] = guard(Code: "#include <f.h>" ); |
284 | TU.AdditionalFiles["system/f.h" ] = guard(Code: "void f();" ); |
285 | TU.AdditionalFiles["system/quoted2_wrapper.h" ] = |
286 | guard(Code: "#include <system/quoted2.h>" ); |
287 | TU.AdditionalFiles["system/quoted2.h" ] = guard(Code: "void quoted2();" ); |
288 | TU.ExtraArgs.push_back(x: "-isystem" + testPath(File: "system" )); |
289 | |
290 | TU.AdditionalFiles["fuzz.h" ] = guard(Code: "#include \"buzz.h\"" ); |
291 | TU.AdditionalFiles["buzz.h" ] = guard(Code: "void buzz();" ); |
292 | |
293 | TU.AdditionalFiles["baz.h" ] = guard(Code: "#include \"private.h\"" ); |
294 | TU.AdditionalFiles["private.h" ] = guard(Code: R"cpp( |
295 | // IWYU pragma: private, include "public.h" |
296 | void foobar(); |
297 | )cpp" ); |
298 | TU.AdditionalFiles["header.h" ] = guard(Code: R"cpp( |
299 | namespace std { class vector {}; } |
300 | )cpp" ); |
301 | |
302 | TU.AdditionalFiles["all.h" ] = guard(Code: "#include \"foo.h\"" ); |
303 | TU.AdditionalFiles["foo.h" ] = guard(Code: R"cpp( |
304 | #define BAR(x) Foo *x |
305 | #define FOO 1 |
306 | struct Foo{}; |
307 | )cpp" ); |
308 | |
309 | TU.Code = MainFile.code(); |
310 | ParsedAST AST = TU.build(); |
311 | |
312 | auto Findings = computeIncludeCleanerFindings(AST); |
313 | Findings.UnusedIncludes.clear(); |
314 | std::vector<clangd::Diag> Diags = issueIncludeCleanerDiagnostics( |
315 | AST, Code: TU.Code, Findings, TFS: MockFS(), |
316 | /*IgnoreHeaders=*/IgnoreHeader: {[](llvm::StringRef ) { |
317 | return Header.ends_with(Suffix: "buzz.h" ); |
318 | }}, |
319 | /*AngledHeaders=*/{[](llvm::StringRef ) { |
320 | return Header.contains(Other: "angled.h" ); |
321 | }}, |
322 | /*QuotedHeaders=*/{[](llvm::StringRef ) { |
323 | return Header.contains(Other: "quoted.h" ) || Header.contains(Other: "quoted2.h" ); |
324 | }}); |
325 | EXPECT_THAT( |
326 | Diags, |
327 | UnorderedElementsAre( |
328 | AllOf(Diag(MainFile.range("b" ), |
329 | "No header providing \"b\" is directly included" ), |
330 | withFix({Fix(MainFile.range("insert_b" ), "#include \"b.h\"\n" , |
331 | "#include \"b.h\"" ), |
332 | FixMessage("add all missing includes" )})), |
333 | AllOf(Diag(MainFile.range("angled" ), |
334 | "No header providing \"angled\" is directly included" ), |
335 | withFix({Fix(MainFile.range("insert_angled" ), |
336 | "#include <angled.h>\n" , "#include <angled.h>" ), |
337 | FixMessage("add all missing includes" )})), |
338 | AllOf( |
339 | Diag(MainFile.range("quoted" ), |
340 | "No header providing \"quoted\" is directly included" ), |
341 | withFix({Fix(MainFile.range("insert_quoted" ), |
342 | "#include \"quoted.h\"\n" , "#include \"quoted.h\"" ), |
343 | FixMessage("add all missing includes" )})), |
344 | AllOf(Diag(MainFile.range("quoted2" ), |
345 | "No header providing \"quoted2\" is directly included" ), |
346 | withFix( |
347 | {Fix(MainFile.range("insert_quoted2" ), |
348 | "#include \"quoted2.h\"\n" , "#include \"quoted2.h\"" ), |
349 | FixMessage("add all missing includes" )})), |
350 | AllOf(Diag(MainFile.range("bar" ), |
351 | "No header providing \"ns::Bar\" is directly included" ), |
352 | withFix({Fix(MainFile.range("insert_d" ), |
353 | "#include \"dir/d.h\"\n" , "#include \"dir/d.h\"" ), |
354 | FixMessage("add all missing includes" )})), |
355 | AllOf(Diag(MainFile.range("f" ), |
356 | "No header providing \"f\" is directly included" ), |
357 | withFix({Fix(MainFile.range("insert_f" ), "#include <f.h>\n" , |
358 | "#include <f.h>" ), |
359 | FixMessage("add all missing includes" )})), |
360 | AllOf( |
361 | Diag(MainFile.range("foobar" ), |
362 | "No header providing \"foobar\" is directly included" ), |
363 | withFix({Fix(MainFile.range("insert_foobar" ), |
364 | "#include \"public.h\"\n" , "#include \"public.h\"" ), |
365 | FixMessage("add all missing includes" )})), |
366 | AllOf( |
367 | Diag(MainFile.range("vector" ), |
368 | "No header providing \"std::vector\" is directly included" ), |
369 | withFix({ |
370 | Fix(MainFile.range("insert_vector" ), "#include <vector>\n" , |
371 | "#include <vector>" ), |
372 | FixMessage("add all missing includes" ), |
373 | })), |
374 | AllOf(Diag(MainFile.range("FOO" ), |
375 | "No header providing \"FOO\" is directly included" ), |
376 | withFix({Fix(MainFile.range("insert_foo" ), |
377 | "#include \"foo.h\"\n" , "#include \"foo.h\"" ), |
378 | FixMessage("add all missing includes" )})), |
379 | AllOf(Diag(MainFile.range("DEF" ), |
380 | "No header providing \"Foo\" is directly included" ), |
381 | withFix({Fix(MainFile.range("insert_foo" ), |
382 | "#include \"foo.h\"\n" , "#include \"foo.h\"" ), |
383 | FixMessage("add all missing includes" )})), |
384 | AllOf(Diag(MainFile.range("BAR" ), |
385 | "No header providing \"BAR\" is directly included" ), |
386 | withFix({Fix(MainFile.range("insert_foo" ), |
387 | "#include \"foo.h\"\n" , "#include \"foo.h\"" ), |
388 | FixMessage("add all missing includes" )})), |
389 | AllOf(Diag(MainFile.range("Foo" ), |
390 | "No header providing \"Foo\" is directly included" ), |
391 | withFix({Fix(MainFile.range("insert_foo" ), |
392 | "#include \"foo.h\"\n" , "#include \"foo.h\"" ), |
393 | FixMessage("add all missing includes" )})))); |
394 | } |
395 | |
396 | TEST(IncludeCleaner, IWYUPragmas) { |
397 | TestTU TU; |
398 | TU.Code = R"cpp( |
399 | #include "behind_keep.h" // IWYU pragma: keep |
400 | #include "exported.h" // IWYU pragma: export |
401 | #include "public.h" |
402 | |
403 | void bar() { foo(); } |
404 | #include "keep_main_file.h" // IWYU pragma: keep |
405 | )cpp" ; |
406 | TU.AdditionalFiles["behind_keep.h" ] = guard(Code: "" ); |
407 | TU.AdditionalFiles["keep_main_file.h" ] = guard(Code: "" ); |
408 | TU.AdditionalFiles["exported.h" ] = guard(Code: "" ); |
409 | TU.AdditionalFiles["public.h" ] = guard(Code: "#include \"private.h\"" ); |
410 | TU.AdditionalFiles["private.h" ] = guard(Code: R"cpp( |
411 | // IWYU pragma: private, include "public.h" |
412 | void foo() {} |
413 | )cpp" ); |
414 | ParsedAST AST = TU.build(); |
415 | IncludeCleanerFindings Findings = computeIncludeCleanerFindings(AST); |
416 | EXPECT_THAT(Findings.UnusedIncludes, IsEmpty()); |
417 | } |
418 | |
419 | TEST(IncludeCleaner, IWYUPragmaExport) { |
420 | TestTU TU; |
421 | TU.Code = R"cpp( |
422 | #include "foo.h" |
423 | )cpp" ; |
424 | TU.AdditionalFiles["foo.h" ] = R"cpp( |
425 | #ifndef FOO_H |
426 | #define FOO_H |
427 | |
428 | #include "bar.h" // IWYU pragma: export |
429 | |
430 | #endif |
431 | )cpp" ; |
432 | TU.AdditionalFiles["bar.h" ] = guard(Code: R"cpp( |
433 | void bar() {} |
434 | )cpp" ); |
435 | ParsedAST AST = TU.build(); |
436 | |
437 | IncludeCleanerFindings Findings = computeIncludeCleanerFindings(AST); |
438 | EXPECT_THAT(Findings.UnusedIncludes, |
439 | ElementsAre(Pointee(writtenInclusion("\"foo.h\"" )))); |
440 | } |
441 | |
442 | TEST(IncludeCleaner, NoDiagsForObjC) { |
443 | TestTU TU; |
444 | TU.Code = R"cpp( |
445 | #include "foo.h" |
446 | |
447 | void bar() {} |
448 | )cpp" ; |
449 | TU.AdditionalFiles["foo.h" ] = R"cpp( |
450 | #ifndef FOO_H |
451 | #define FOO_H |
452 | |
453 | #endif |
454 | )cpp" ; |
455 | TU.ExtraArgs.emplace_back(args: "-xobjective-c" ); |
456 | |
457 | ParsedAST AST = TU.build(); |
458 | IncludeCleanerFindings Findings = computeIncludeCleanerFindings(AST); |
459 | EXPECT_THAT(Findings.MissingIncludes, IsEmpty()); |
460 | EXPECT_THAT(Findings.UnusedIncludes, IsEmpty()); |
461 | } |
462 | |
463 | TEST(IncludeCleaner, UmbrellaUsesPrivate) { |
464 | TestTU TU; |
465 | TU.Code = R"cpp( |
466 | #include "private.h" |
467 | )cpp" ; |
468 | TU.AdditionalFiles["private.h" ] = guard(Code: R"cpp( |
469 | // IWYU pragma: private, include "public.h" |
470 | void foo() {} |
471 | )cpp" ); |
472 | TU.Filename = "public.h" ; |
473 | ParsedAST AST = TU.build(); |
474 | IncludeCleanerFindings Findings = computeIncludeCleanerFindings(AST); |
475 | EXPECT_THAT(Findings.UnusedIncludes, IsEmpty()); |
476 | } |
477 | |
478 | TEST(IncludeCleaner, MacroExpandedThroughIncludes) { |
479 | Annotations MainFile(R"cpp( |
480 | #include "all.h" |
481 | #define FOO(X) const Foo *X |
482 | void foo() { |
483 | #include [["expander.inc"]] |
484 | } |
485 | )cpp" ); |
486 | |
487 | TestTU TU; |
488 | TU.AdditionalFiles["expander.inc" ] = guard(Code: "FOO(f1);FOO(f2);" ); |
489 | TU.AdditionalFiles["foo.h" ] = guard(Code: "struct Foo {};" ); |
490 | TU.AdditionalFiles["all.h" ] = guard(Code: "#include \"foo.h\"" ); |
491 | |
492 | TU.Code = MainFile.code(); |
493 | ParsedAST AST = TU.build(); |
494 | |
495 | auto Findings = computeIncludeCleanerFindings(AST).MissingIncludes; |
496 | EXPECT_THAT(Findings, testing::SizeIs(1)); |
497 | auto RefRange = Findings.front().SymRefRange; |
498 | auto &SM = AST.getSourceManager(); |
499 | EXPECT_EQ(RefRange.file(), SM.getMainFileID()); |
500 | // FIXME: Point at the spelling location, rather than the include. |
501 | EXPECT_EQ(halfOpenToRange(SM, RefRange.toCharRange(SM)), MainFile.range()); |
502 | } |
503 | |
504 | TEST(IncludeCleaner, MissingIncludesAreUnique) { |
505 | Annotations MainFile(R"cpp( |
506 | #include "all.h" |
507 | FOO([[Foo]]); |
508 | )cpp" ); |
509 | |
510 | TestTU TU; |
511 | TU.AdditionalFiles["foo.h" ] = guard(Code: "struct Foo {};" ); |
512 | TU.AdditionalFiles["all.h" ] = guard(Code: R"cpp( |
513 | #include "foo.h" |
514 | #define FOO(X) X y; X z |
515 | )cpp" ); |
516 | |
517 | TU.Code = MainFile.code(); |
518 | ParsedAST AST = TU.build(); |
519 | |
520 | auto Findings = computeIncludeCleanerFindings(AST).MissingIncludes; |
521 | EXPECT_THAT(Findings, testing::SizeIs(1)); |
522 | auto RefRange = Findings.front().SymRefRange; |
523 | auto &SM = AST.getSourceManager(); |
524 | EXPECT_EQ(RefRange.file(), SM.getMainFileID()); |
525 | EXPECT_EQ(halfOpenToRange(SM, RefRange.toCharRange(SM)), MainFile.range()); |
526 | } |
527 | |
528 | TEST(IncludeCleaner, NoCrash) { |
529 | TestTU TU; |
530 | Annotations MainCode(R"cpp( |
531 | #include "all.h" |
532 | void test() { |
533 | [[1s]]; |
534 | } |
535 | )cpp" ); |
536 | TU.Code = MainCode.code(); |
537 | TU.AdditionalFiles["foo.h" ] = |
538 | guard(Code: "int operator\"\"s(unsigned long long) { return 0; }" ); |
539 | TU.AdditionalFiles["all.h" ] = guard(Code: "#include \"foo.h\"" ); |
540 | ParsedAST AST = TU.build(); |
541 | const auto &MissingIncludes = |
542 | computeIncludeCleanerFindings(AST).MissingIncludes; |
543 | EXPECT_THAT(MissingIncludes, testing::SizeIs(1)); |
544 | auto &SM = AST.getSourceManager(); |
545 | EXPECT_EQ( |
546 | halfOpenToRange(SM, MissingIncludes.front().SymRefRange.toCharRange(SM)), |
547 | MainCode.range()); |
548 | } |
549 | |
550 | TEST(IncludeCleaner, IsPreferredProvider) { |
551 | auto TU = TestTU::withCode(Code: R"cpp( |
552 | #include "decl.h" |
553 | #include "def.h" |
554 | #include "def.h" |
555 | )cpp" ); |
556 | TU.AdditionalFiles["decl.h" ] = "" ; |
557 | TU.AdditionalFiles["def.h" ] = "" ; |
558 | |
559 | auto AST = TU.build(); |
560 | auto &IncludeDecl = AST.getIncludeStructure().MainFileIncludes[0]; |
561 | auto &IncludeDef1 = AST.getIncludeStructure().MainFileIncludes[1]; |
562 | auto &IncludeDef2 = AST.getIncludeStructure().MainFileIncludes[2]; |
563 | |
564 | auto &FM = AST.getSourceManager().getFileManager(); |
565 | auto DeclH = *FM.getOptionalFileRef(Filename: "decl.h" ); |
566 | auto DefH = *FM.getOptionalFileRef(Filename: "def.h" ); |
567 | |
568 | auto Includes = convertIncludes(AST); |
569 | std::vector<include_cleaner::Header> Providers = { |
570 | include_cleaner::Header(DefH), include_cleaner::Header(DeclH)}; |
571 | EXPECT_FALSE(isPreferredProvider(IncludeDecl, Includes, Providers)); |
572 | EXPECT_TRUE(isPreferredProvider(IncludeDef1, Includes, Providers)); |
573 | EXPECT_TRUE(isPreferredProvider(IncludeDef2, Includes, Providers)); |
574 | } |
575 | |
576 | TEST(IncludeCleaner, BatchFix) { |
577 | TestTU TU; |
578 | TU.Filename = "main.cpp" ; |
579 | TU.AdditionalFiles["foo.h" ] = guard(Code: "class Foo;" ); |
580 | TU.AdditionalFiles["bar.h" ] = guard(Code: "class Bar;" ); |
581 | TU.AdditionalFiles["all.h" ] = guard(Code: R"cpp( |
582 | #include "foo.h" |
583 | #include "bar.h" |
584 | )cpp" ); |
585 | |
586 | TU.Code = R"cpp( |
587 | #include "all.h" |
588 | |
589 | Foo* foo; |
590 | )cpp" ; |
591 | auto AST = TU.build(); |
592 | EXPECT_THAT( |
593 | issueIncludeCleanerDiagnostics( |
594 | AST, TU.Code, computeIncludeCleanerFindings(AST), MockFS()), |
595 | UnorderedElementsAre(withFix({FixMessage("#include \"foo.h\"" ), |
596 | FixMessage("fix all includes" )}), |
597 | withFix({FixMessage("remove #include directive" ), |
598 | FixMessage("fix all includes" )}))); |
599 | |
600 | TU.Code = R"cpp( |
601 | #include "all.h" |
602 | #include "bar.h" |
603 | |
604 | Foo* foo; |
605 | )cpp" ; |
606 | AST = TU.build(); |
607 | EXPECT_THAT( |
608 | issueIncludeCleanerDiagnostics( |
609 | AST, TU.Code, computeIncludeCleanerFindings(AST), MockFS()), |
610 | UnorderedElementsAre(withFix({FixMessage("#include \"foo.h\"" ), |
611 | FixMessage("fix all includes" )}), |
612 | withFix({FixMessage("remove #include directive" ), |
613 | FixMessage("remove all unused includes" ), |
614 | FixMessage("fix all includes" )}), |
615 | withFix({FixMessage("remove #include directive" ), |
616 | FixMessage("remove all unused includes" ), |
617 | FixMessage("fix all includes" )}))); |
618 | |
619 | TU.Code = R"cpp( |
620 | #include "all.h" |
621 | |
622 | Foo* foo; |
623 | Bar* bar; |
624 | )cpp" ; |
625 | AST = TU.build(); |
626 | EXPECT_THAT( |
627 | issueIncludeCleanerDiagnostics( |
628 | AST, TU.Code, computeIncludeCleanerFindings(AST), MockFS()), |
629 | UnorderedElementsAre(withFix({FixMessage("#include \"foo.h\"" ), |
630 | FixMessage("add all missing includes" ), |
631 | FixMessage("fix all includes" )}), |
632 | withFix({FixMessage("#include \"bar.h\"" ), |
633 | FixMessage("add all missing includes" ), |
634 | FixMessage("fix all includes" )}), |
635 | withFix({FixMessage("remove #include directive" ), |
636 | FixMessage("fix all includes" )}))); |
637 | } |
638 | |
639 | // In the presence of IWYU pragma private, we should accept spellings other |
640 | // than the recommended one if they appear to name the same public header. |
641 | TEST(IncludeCleaner, VerbatimEquivalence) { |
642 | auto TU = TestTU::withCode(Code: R"cpp( |
643 | #include "lib/rel/public.h" |
644 | int x = Public; |
645 | )cpp" ); |
646 | TU.AdditionalFiles["repo/lib/rel/private.h" ] = R"cpp( |
647 | #pragma once |
648 | // IWYU pragma: private, include "rel/public.h" |
649 | int Public; |
650 | )cpp" ; |
651 | TU.AdditionalFiles["repo/lib/rel/public.h" ] = R"cpp( |
652 | #pragma once |
653 | #include "rel/private.h" |
654 | )cpp" ; |
655 | |
656 | TU.ExtraArgs.push_back(x: "-Irepo" ); |
657 | TU.ExtraArgs.push_back(x: "-Irepo/lib" ); |
658 | |
659 | auto AST = TU.build(); |
660 | auto Findings = computeIncludeCleanerFindings(AST); |
661 | EXPECT_THAT(Findings.MissingIncludes, IsEmpty()); |
662 | EXPECT_THAT(Findings.UnusedIncludes, IsEmpty()); |
663 | } |
664 | |
665 | TEST(IncludeCleaner, ResourceDirIsIgnored) { |
666 | auto TU = TestTU::withCode(Code: R"cpp( |
667 | #include <amintrin.h> |
668 | #include <imintrin.h> |
669 | void baz() { |
670 | bar(); |
671 | } |
672 | )cpp" ); |
673 | TU.ExtraArgs.push_back(x: "-resource-dir" ); |
674 | TU.ExtraArgs.push_back(x: testPath(File: "resources" )); |
675 | TU.AdditionalFiles["resources/include/amintrin.h" ] = guard(Code: "" ); |
676 | TU.AdditionalFiles["resources/include/imintrin.h" ] = guard(Code: R"cpp( |
677 | #include <emintrin.h> |
678 | )cpp" ); |
679 | TU.AdditionalFiles["resources/include/emintrin.h" ] = guard(Code: R"cpp( |
680 | void bar(); |
681 | )cpp" ); |
682 | auto AST = TU.build(); |
683 | auto Findings = computeIncludeCleanerFindings(AST); |
684 | EXPECT_THAT(Findings.UnusedIncludes, IsEmpty()); |
685 | EXPECT_THAT(Findings.MissingIncludes, IsEmpty()); |
686 | } |
687 | |
688 | TEST(IncludeCleaner, DifferentHeaderSameSpelling) { |
689 | // `foo` is declared in foo_inner/foo.h, but there's no way to spell it |
690 | // directly. Make sure we don't generate unusued/missing include findings in |
691 | // such cases. |
692 | auto TU = TestTU::withCode(Code: R"cpp( |
693 | #include <foo.h> |
694 | void baz() { |
695 | foo(); |
696 | } |
697 | )cpp" ); |
698 | TU.AdditionalFiles["foo/foo.h" ] = guard(Code: "#include_next <foo.h>" ); |
699 | TU.AdditionalFiles["foo_inner/foo.h" ] = guard(Code: R"cpp( |
700 | void foo(); |
701 | )cpp" ); |
702 | TU.ExtraArgs.push_back(x: "-Ifoo" ); |
703 | TU.ExtraArgs.push_back(x: "-Ifoo_inner" ); |
704 | |
705 | auto AST = TU.build(); |
706 | auto Findings = computeIncludeCleanerFindings(AST); |
707 | EXPECT_THAT(Findings.UnusedIncludes, IsEmpty()); |
708 | EXPECT_THAT(Findings.MissingIncludes, IsEmpty()); |
709 | } |
710 | } // namespace |
711 | } // namespace clangd |
712 | } // namespace clang |
713 | |