#include "IncludeCleaner.h"
#include "Config.h"
+#include "Diagnostics.h"
#include "Headers.h"
#include "ParsedAST.h"
#include "Protocol.h"
#include "SourceCode.h"
+#include "URI.h"
#include "clang-include-cleaner/Analysis.h"
#include "clang-include-cleaner/Types.h"
#include "index/CanonicalIncludes.h"
#include "support/Logger.h"
+#include "support/Path.h"
#include "support/Trace.h"
#include "clang/AST/ASTContext.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/AST/Expr.h"
#include "clang/AST/ExprCXX.h"
#include "clang/AST/RecursiveASTVisitor.h"
+#include "clang/AST/TemplateName.h"
+#include "clang/AST/Type.h"
+#include "clang/Basic/Diagnostic.h"
+#include "clang/Basic/LLVM.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Basic/SourceManager.h"
+#include "clang/Format/Format.h"
#include "clang/Lex/HeaderSearch.h"
#include "clang/Lex/Preprocessor.h"
+#include "clang/Tooling/Core/Replacement.h"
+#include "clang/Tooling/Inclusions/HeaderIncludes.h"
+#include "clang/Tooling/Inclusions/IncludeStyle.h"
+#include "clang/Tooling/Inclusions/StandardLibrary.h"
#include "clang/Tooling/Syntax/Tokens.h"
#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/DenseSet.h"
+#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/STLFunctionalExtras.h"
#include "llvm/ADT/SmallString.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/StringSet.h"
+#include "llvm/Support/Casting.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/ErrorHandling.h"
#include "llvm/Support/FormatVariadic.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/Regex.h"
-#include <functional>
+#include <iterator>
#include <optional>
+#include <string>
+#include <vector>
namespace clang {
namespace clangd {
}
}
+bool isFilteredByConfig(const Config &Cfg, llvm::StringRef HeaderPath) {
+ // Convert the path to Unix slashes and try to match against the filter.
+ llvm::SmallString<64> NormalizedPath(HeaderPath);
+ llvm::sys::path::native(NormalizedPath, llvm::sys::path::Style::posix);
+ for (auto &Filter : Cfg.Diagnostics.Includes.IgnoreHeader) {
+ if (Filter(NormalizedPath))
+ return true;
+ }
+ return false;
+}
+
static bool mayConsiderUnused(const Inclusion &Inc, ParsedAST &AST,
const Config &Cfg) {
if (Inc.BehindPragmaKeep)
FE->getName());
return false;
}
- for (auto &Filter : Cfg.Diagnostics.Includes.IgnoreHeader) {
- // Convert the path to Unix slashes and try to match against the filter.
- llvm::SmallString<64> Path(Inc.Resolved);
- llvm::sys::path::native(Path, llvm::sys::path::Style::posix);
- if (Filter(Path)) {
- dlog("{0} header is filtered out by the configuration", FE->getName());
- return false;
- }
+
+ if (isFilteredByConfig(Cfg, Inc.Resolved)) {
+ dlog("{0} header is filtered out by the configuration", FE->getName());
+ return false;
}
return true;
}
return ID;
}
+include_cleaner::Includes
+convertIncludes(const SourceManager &SM,
+ const llvm::ArrayRef<Inclusion> MainFileIncludes) {
+ include_cleaner::Includes Includes;
+ for (const Inclusion &Inc : MainFileIncludes) {
+ include_cleaner::Include TransformedInc;
+ llvm::StringRef WrittenRef = llvm::StringRef(Inc.Written);
+ TransformedInc.Spelled = WrittenRef.trim("\"<>");
+ TransformedInc.HashLocation =
+ SM.getComposedLoc(SM.getMainFileID(), Inc.HashOffset);
+ TransformedInc.Line = Inc.HashLine + 1;
+ TransformedInc.Angled = WrittenRef.starts_with("<");
+ auto FE = SM.getFileManager().getFile(Inc.Resolved);
+ if (!FE) {
+ elog("IncludeCleaner: Failed to get an entry for resolved path {0}: {1}",
+ Inc.Resolved, FE.getError().message());
+ continue;
+ }
+ TransformedInc.Resolved = *FE;
+ Includes.add(std::move(TransformedInc));
+ }
+ return Includes;
+}
+
+std::string spellHeader(ParsedAST &AST, const FileEntry *MainFile,
+ include_cleaner::Header Provider) {
+ if (Provider.kind() == include_cleaner::Header::Physical) {
+ if (auto CanonicalPath =
+ getCanonicalPath(Provider.physical(), AST.getSourceManager())) {
+ std::string SpelledHeader =
+ llvm::cantFail(URI::includeSpelling(URI::create(*CanonicalPath)));
+ if (!SpelledHeader.empty())
+ return SpelledHeader;
+ }
+ }
+ return include_cleaner::spellHeader(
+ Provider, AST.getPreprocessor().getHeaderSearchInfo(), MainFile);
+}
+
+std::vector<include_cleaner::SymbolReference>
+collectMacroReferences(ParsedAST &AST) {
+ const auto &SM = AST.getSourceManager();
+ // FIXME: !!this is a hacky way to collect macro references.
+ std::vector<include_cleaner::SymbolReference> Macros;
+ auto &PP = AST.getPreprocessor();
+ for (const syntax::Token &Tok :
+ AST.getTokens().spelledTokens(SM.getMainFileID())) {
+ auto Macro = locateMacroAt(Tok, PP);
+ if (!Macro)
+ continue;
+ if (auto DefLoc = Macro->Info->getDefinitionLoc(); DefLoc.isValid())
+ Macros.push_back(
+ {Tok.location(),
+ include_cleaner::Macro{/*Name=*/PP.getIdentifierInfo(Tok.text(SM)),
+ DefLoc},
+ include_cleaner::RefType::Explicit});
+ }
+ return Macros;
+}
+
+llvm::StringRef getResolvedPath(const include_cleaner::Header &SymProvider) {
+ switch (SymProvider.kind()) {
+ case include_cleaner::Header::Physical:
+ return SymProvider.physical()->tryGetRealPathName();
+ case include_cleaner::Header::Standard:
+ return SymProvider.standard().name().trim("<>\"");
+ case include_cleaner::Header::Verbatim:
+ return SymProvider.verbatim().trim("<>\"");
+ }
+ llvm_unreachable("Unknown header kind");
+}
+
+std::string getSymbolName(const include_cleaner::Symbol &Sym) {
+ switch (Sym.kind()) {
+ case include_cleaner::Symbol::Macro:
+ return Sym.macro().Name->getName().str();
+ case include_cleaner::Symbol::Declaration:
+ return llvm::dyn_cast<NamedDecl>(&Sym.declaration())
+ ->getQualifiedNameAsString();
+ }
+ llvm_unreachable("Unknown symbol kind");
+}
+
+std::vector<Diag> generateMissingIncludeDiagnostics(
+ ParsedAST &AST, llvm::ArrayRef<MissingIncludeDiagInfo> MissingIncludes,
+ llvm::StringRef Code) {
+ std::vector<Diag> Result;
+ const Config &Cfg = Config::current();
+ if (Cfg.Diagnostics.MissingIncludes != Config::IncludesPolicy::Strict ||
+ Cfg.Diagnostics.SuppressAll ||
+ Cfg.Diagnostics.Suppress.contains("missing-includes")) {
+ return Result;
+ }
+
+ const SourceManager &SM = AST.getSourceManager();
+ const FileEntry *MainFile = SM.getFileEntryForID(SM.getMainFileID());
+
+ auto FileStyle = format::getStyle(
+ format::DefaultFormatStyle, AST.tuPath(), format::DefaultFallbackStyle,
+ Code, &SM.getFileManager().getVirtualFileSystem());
+ if (!FileStyle) {
+ elog("Couldn't infer style", FileStyle.takeError());
+ FileStyle = format::getLLVMStyle();
+ }
+
+ tooling::HeaderIncludes HeaderIncludes(AST.tuPath(), Code,
+ FileStyle->IncludeStyle);
+ for (const auto &SymbolWithMissingInclude : MissingIncludes) {
+ llvm::StringRef ResolvedPath =
+ getResolvedPath(SymbolWithMissingInclude.Providers.front());
+ if (isFilteredByConfig(Cfg, ResolvedPath)) {
+ dlog("IncludeCleaner: not diagnosing missing include {0}, filtered by "
+ "config",
+ ResolvedPath);
+ continue;
+ }
+
+ std::string Spelling =
+ spellHeader(AST, MainFile, SymbolWithMissingInclude.Providers.front());
+ llvm::StringRef HeaderRef{Spelling};
+ bool Angled = HeaderRef.starts_with("<");
+ // We might suggest insertion of an existing include in edge cases, e.g.,
+ // include is present in a PP-disabled region, or spelling of the header
+ // turns out to be the same as one of the unresolved includes in the
+ // main file.
+ std::optional<tooling::Replacement> Replacement = HeaderIncludes.insert(
+ HeaderRef.trim("\"<>"), Angled, tooling::IncludeDirective::Include);
+ if (!Replacement.has_value())
+ continue;
+
+ Diag &D = Result.emplace_back();
+ D.Message =
+ llvm::formatv("No header providing \"{0}\" is directly included",
+ getSymbolName(SymbolWithMissingInclude.Symbol));
+ D.Name = "missing-includes";
+ D.Source = Diag::DiagSource::Clangd;
+ D.File = AST.tuPath();
+ D.InsideMainFile = true;
+ D.Severity = DiagnosticsEngine::Warning;
+ D.Range = clangd::Range{
+ offsetToPosition(Code,
+ SymbolWithMissingInclude.SymRefRange.beginOffset()),
+ offsetToPosition(Code,
+ SymbolWithMissingInclude.SymRefRange.endOffset())};
+ auto &F = D.Fixes.emplace_back();
+ F.Message = "#include " + Spelling;
+ TextEdit Edit = replacementToEdit(Code, *Replacement);
+ F.Edits.emplace_back(std::move(Edit));
+ }
+ return Result;
+}
+
+std::vector<Diag> generateUnusedIncludeDiagnostics(
+ PathRef FileName, llvm::ArrayRef<const Inclusion *> UnusedIncludes,
+ llvm::StringRef Code) {
+ std::vector<Diag> Result;
+ const Config &Cfg = Config::current();
+ if (Cfg.Diagnostics.UnusedIncludes == Config::IncludesPolicy::None ||
+ Cfg.Diagnostics.SuppressAll ||
+ Cfg.Diagnostics.Suppress.contains("unused-includes")) {
+ return Result;
+ }
+ for (const auto *Inc : UnusedIncludes) {
+ Diag &D = Result.emplace_back();
+ D.Message =
+ llvm::formatv("included header {0} is not used directly",
+ llvm::sys::path::filename(
+ Inc->Written.substr(1, Inc->Written.size() - 2),
+ llvm::sys::path::Style::posix));
+ D.Name = "unused-includes";
+ D.Source = Diag::DiagSource::Clangd;
+ D.File = FileName;
+ D.InsideMainFile = true;
+ D.Severity = DiagnosticsEngine::Warning;
+ D.Tags.push_back(Unnecessary);
+ D.Range = getDiagnosticRange(Code, Inc->HashOffset);
+ // FIXME(kirillbobyrev): Removing inclusion might break the code if the
+ // used headers are only reachable transitively through this one. Suggest
+ // including them directly instead.
+ // FIXME(kirillbobyrev): Add fix suggestion for adding IWYU pragmas
+ // (keep/export) remove the warning once we support IWYU pragmas.
+ auto &F = D.Fixes.emplace_back();
+ F.Message = "remove #include directive";
+ F.Edits.emplace_back();
+ F.Edits.back().range.start.line = Inc->HashLine;
+ F.Edits.back().range.end.line = Inc->HashLine + 1;
+ }
+ return Result;
+}
} // namespace
ReferencedLocations findReferencedLocations(ASTContext &Ctx, Preprocessor &PP,
translateToHeaderIDs(ReferencedFiles, AST.getIncludeStructure(), SM);
return getUnused(AST, ReferencedHeaders, ReferencedFiles.SpelledUmbrellas);
}
-std::vector<const Inclusion *>
-computeUnusedIncludesExperimental(ParsedAST &AST) {
+
+IncludeCleanerFindings computeIncludeCleanerFindings(ParsedAST &AST) {
const auto &SM = AST.getSourceManager();
const auto &Includes = AST.getIncludeStructure();
+ include_cleaner::Includes ConvertedIncludes =
+ convertIncludes(SM, Includes.MainFileIncludes);
+ const FileEntry *MainFile = SM.getFileEntryForID(SM.getMainFileID());
- // FIXME: !!this is a hacky way to collect macro references.
- std::vector<include_cleaner::SymbolReference> Macros;
- auto &PP = AST.getPreprocessor();
- for (const syntax::Token &Tok :
- AST.getTokens().spelledTokens(SM.getMainFileID())) {
- auto Macro = locateMacroAt(Tok, PP);
- if (!Macro)
- continue;
- if (auto DefLoc = Macro->Info->getDefinitionLoc(); DefLoc.isValid())
- Macros.push_back(
- {Tok.location(),
- include_cleaner::Macro{/*Name=*/PP.getIdentifierInfo(Tok.text(SM)),
- DefLoc},
- include_cleaner::RefType::Explicit});
- }
+ std::vector<include_cleaner::SymbolReference> Macros =
+ collectMacroReferences(AST);
+ std::vector<MissingIncludeDiagInfo> MissingIncludes;
llvm::DenseSet<IncludeStructure::HeaderID> Used;
+ trace::Span Tracer("include_cleaner::walkUsed");
include_cleaner::walkUsed(
AST.getLocalTopLevelDecls(), /*MacroRefs=*/Macros,
AST.getPragmaIncludes(), SM,
[&](const include_cleaner::SymbolReference &Ref,
llvm::ArrayRef<include_cleaner::Header> Providers) {
+ bool Satisfied = false;
for (const auto &H : Providers) {
- switch (H.kind()) {
- case include_cleaner::Header::Physical:
- if (auto HeaderID = Includes.getID(H.physical()))
- Used.insert(*HeaderID);
- break;
- case include_cleaner::Header::Standard:
- for (auto HeaderID : Includes.StdlibHeaders.lookup(H.standard()))
- Used.insert(HeaderID);
- break;
- case include_cleaner::Header::Verbatim:
- for (auto *Inc :
- Includes.mainFileIncludesWithSpelling(H.verbatim())) {
- if (!Inc->HeaderID.has_value())
- continue;
- IncludeStructure::HeaderID ID =
- static_cast<IncludeStructure::HeaderID>(*Inc->HeaderID);
- Used.insert(ID);
- }
- break;
+ if (H.kind() == include_cleaner::Header::Physical &&
+ H.physical() == MainFile) {
+ Satisfied = true;
+ continue;
+ }
+ for (auto *Inc : ConvertedIncludes.match(H)) {
+ Satisfied = true;
+ auto HeaderID = Includes.getID(Inc->Resolved);
+ assert(HeaderID.has_value() &&
+ "ConvertedIncludes only contains resolved includes.");
+ Used.insert(*HeaderID);
}
}
+
+ if (Satisfied || Providers.empty() ||
+ Ref.RT != include_cleaner::RefType::Explicit)
+ return;
+
+ auto &Tokens = AST.getTokens();
+ auto SpelledForExpanded =
+ Tokens.spelledForExpanded(Tokens.expandedTokens(Ref.RefLocation));
+ if (!SpelledForExpanded)
+ return;
+
+ auto Range = syntax::Token::range(SM, SpelledForExpanded->front(),
+ SpelledForExpanded->back());
+ MissingIncludeDiagInfo DiagInfo{Ref.Target, Range, Providers};
+ MissingIncludes.push_back(std::move(DiagInfo));
});
- return getUnused(AST, Used, /*ReferencedPublicHeaders*/ {});
+ std::vector<const Inclusion *> UnusedIncludes =
+ getUnused(AST, Used, /*ReferencedPublicHeaders*/ {});
+ return {std::move(UnusedIncludes), std::move(MissingIncludes)};
}
-std::vector<Diag> issueUnusedIncludesDiagnostics(ParsedAST &AST,
+std::vector<Diag> issueIncludeCleanerDiagnostics(ParsedAST &AST,
llvm::StringRef Code) {
- const Config &Cfg = Config::current();
- if (Cfg.Diagnostics.UnusedIncludes == Config::UnusedIncludesPolicy::None ||
- Cfg.Diagnostics.SuppressAll ||
- Cfg.Diagnostics.Suppress.contains("unused-includes"))
- return {};
// Interaction is only polished for C/CPP.
if (AST.getLangOpts().ObjC)
return {};
- trace::Span Tracer("IncludeCleaner::issueUnusedIncludesDiagnostics");
- std::vector<Diag> Result;
- std::string FileName =
- AST.getSourceManager()
- .getFileEntryRefForID(AST.getSourceManager().getMainFileID())
- ->getName()
- .str();
- const auto &UnusedIncludes =
- Cfg.Diagnostics.UnusedIncludes == Config::UnusedIncludesPolicy::Experiment
- ? computeUnusedIncludesExperimental(AST)
- : computeUnusedIncludes(AST);
- for (const auto *Inc : UnusedIncludes) {
- Diag D;
- D.Message =
- llvm::formatv("included header {0} is not used directly",
- llvm::sys::path::filename(
- Inc->Written.substr(1, Inc->Written.size() - 2),
- llvm::sys::path::Style::posix));
- D.Name = "unused-includes";
- D.Source = Diag::DiagSource::Clangd;
- D.File = FileName;
- D.Severity = DiagnosticsEngine::Warning;
- D.Tags.push_back(Unnecessary);
- D.Range = getDiagnosticRange(Code, Inc->HashOffset);
- // FIXME(kirillbobyrev): Removing inclusion might break the code if the
- // used headers are only reachable transitively through this one. Suggest
- // including them directly instead.
- // FIXME(kirillbobyrev): Add fix suggestion for adding IWYU pragmas
- // (keep/export) remove the warning once we support IWYU pragmas.
- D.Fixes.emplace_back();
- D.Fixes.back().Message = "remove #include directive";
- D.Fixes.back().Edits.emplace_back();
- D.Fixes.back().Edits.back().range.start.line = Inc->HashLine;
- D.Fixes.back().Edits.back().range.end.line = Inc->HashLine + 1;
- D.InsideMainFile = true;
- Result.push_back(std::move(D));
+
+ trace::Span Tracer("IncludeCleaner::issueIncludeCleanerDiagnostics");
+
+ const Config &Cfg = Config::current();
+ IncludeCleanerFindings Findings;
+ if (Cfg.Diagnostics.MissingIncludes == Config::IncludesPolicy::Strict ||
+ Cfg.Diagnostics.UnusedIncludes == Config::IncludesPolicy::Experiment) {
+ // will need include-cleaner results, call it once
+ Findings = computeIncludeCleanerFindings(AST);
}
+
+ std::vector<Diag> Result = generateUnusedIncludeDiagnostics(
+ AST.tuPath(),
+ Cfg.Diagnostics.UnusedIncludes == Config::IncludesPolicy::Strict
+ ? computeUnusedIncludes(AST)
+ : Findings.UnusedIncludes,
+ Code);
+ llvm::move(
+ generateMissingIncludeDiagnostics(AST, Findings.MissingIncludes, Code),
+ std::back_inserter(Result));
return Result;
}
#include "Annotations.h"
#include "Config.h"
+#include "Diagnostics.h"
#include "IncludeCleaner.h"
+#include "ParsedAST.h"
#include "SourceCode.h"
#include "TestFS.h"
#include "TestTU.h"
+#include "clang-include-cleaner/Analysis.h"
+#include "clang-include-cleaner/Types.h"
#include "support/Context.h"
+#include "clang/AST/DeclBase.h"
+#include "clang/Basic/SourceManager.h"
+#include "clang/Tooling/Syntax/Tokens.h"
#include "llvm/ADT/ScopeExit.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Casting.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/ScopedPrinter.h"
#include "llvm/Testing/Support/SupportHelpers.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
+#include <cstddef>
+#include <string>
+#include <vector>
namespace clang {
namespace clangd {
namespace {
+using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::ElementsAreArray;
using ::testing::IsEmpty;
+using ::testing::Matcher;
using ::testing::Pointee;
using ::testing::UnorderedElementsAre;
+Matcher<const Diag &> withFix(::testing::Matcher<Fix> FixMatcher) {
+ return Field(&Diag::Fixes, ElementsAre(FixMatcher));
+}
+
+MATCHER_P2(Diag, Range, Message,
+ "Diag at " + llvm::to_string(Range) + " = [" + Message + "]") {
+ return arg.Range == Range && arg.Message == Message;
+}
+
+MATCHER_P3(Fix, Range, Replacement, Message,
+ "Fix " + llvm::to_string(Range) + " => " +
+ ::testing::PrintToString(Replacement) + " = [" + Message + "]") {
+ return arg.Message == Message && arg.Edits.size() == 1 &&
+ arg.Edits[0].range == Range && arg.Edits[0].newText == Replacement;
+}
+
std::string guard(llvm::StringRef Code) {
return "#pragma once\n" + Code.str();
}
auto AST = TU.build();
EXPECT_THAT(computeUnusedIncludes(AST),
ElementsAre(Pointee(writtenInclusion("<queue>"))));
- EXPECT_THAT(computeUnusedIncludesExperimental(AST),
+ IncludeCleanerFindings Findings = computeIncludeCleanerFindings(AST);
+ EXPECT_THAT(Findings.UnusedIncludes,
ElementsAre(Pointee(writtenInclusion("<queue>"))));
}
computeUnusedIncludes(AST),
UnorderedElementsAre(Pointee(writtenInclusion("\"unused.h\"")),
Pointee(writtenInclusion("\"dir/unused.h\""))));
+
+ IncludeCleanerFindings Findings = computeIncludeCleanerFindings(AST);
EXPECT_THAT(
- computeUnusedIncludesExperimental(AST),
+ Findings.UnusedIncludes,
UnorderedElementsAre(Pointee(writtenInclusion("\"unused.h\"")),
Pointee(writtenInclusion("\"dir/unused.h\""))));
}
+TEST(IncludeCleaner, ComputeMissingHeaders) {
+ Annotations MainFile(R"cpp(
+ #include "a.h"
+
+ void foo() {
+ $b[[b]]();
+ })cpp");
+ TestTU TU;
+ TU.Filename = "foo.cpp";
+ TU.AdditionalFiles["a.h"] = guard("#include \"b.h\"");
+ TU.AdditionalFiles["b.h"] = guard("void b();");
+
+ TU.Code = MainFile.code();
+ ParsedAST AST = TU.build();
+
+ IncludeCleanerFindings Findings = computeIncludeCleanerFindings(AST);
+ const SourceManager &SM = AST.getSourceManager();
+ const NamedDecl *BDecl = nullptr;
+ for (Decl *D : AST.getASTContext().getTranslationUnitDecl()->decls()) {
+ const NamedDecl *CandidateDecl = llvm::dyn_cast<NamedDecl>(D);
+ std::string Name = CandidateDecl->getQualifiedNameAsString();
+ if (Name != "b")
+ continue;
+ BDecl = CandidateDecl;
+ }
+ ASSERT_TRUE(BDecl);
+ include_cleaner::Symbol B{*BDecl};
+ auto Range = MainFile.range("b");
+ size_t Start = llvm::cantFail(positionToOffset(MainFile.code(), Range.start));
+ size_t End = llvm::cantFail(positionToOffset(MainFile.code(), Range.end));
+ syntax::FileRange BRange{SM.getMainFileID(), static_cast<unsigned int>(Start),
+ static_cast<unsigned int>(End)};
+ include_cleaner::Header Header{*SM.getFileManager().getFile("b.h")};
+ MissingIncludeDiagInfo BInfo{B, BRange, {Header}};
+ EXPECT_THAT(Findings.MissingIncludes, ElementsAre(BInfo));
+}
+
+TEST(IncludeCleaner, GenerateMissingHeaderDiags) {
+ Config Cfg;
+ Cfg.Diagnostics.MissingIncludes = Config::IncludesPolicy::Strict;
+ Cfg.Diagnostics.Includes.IgnoreHeader = {[](llvm::StringRef Header) {
+ return Header == testPath("buzz.h", llvm::sys::path::Style::posix);
+ }};
+ WithContextValue Ctx(Config::Key, std::move(Cfg));
+ Annotations MainFile(R"cpp(
+#include "a.h"
+$insert_b[[]]#include "baz.h"
+#include "dir/c.h"
+$insert_d[[]]#include "fuzz.h"
+#include "header.h"
+$insert_foobar[[]]#include <e.h>
+$insert_f[[]]$insert_vector[[]]
+
+ void foo() {
+ $b[[b]]();
+
+ ns::$bar[[Bar]] bar;
+ bar.d();
+ $f[[f]]();
+
+ // this should not be diagnosed, because it's ignored in the config
+ buzz();
+
+ $foobar[[foobar]]();
+
+ std::$vector[[vector]] v;
+ })cpp");
+
+ TestTU TU;
+ TU.Filename = "foo.cpp";
+ TU.AdditionalFiles["a.h"] = guard("#include \"b.h\"");
+ TU.AdditionalFiles["b.h"] = guard("void b();");
+
+ TU.AdditionalFiles["dir/c.h"] = guard("#include \"d.h\"");
+ TU.AdditionalFiles["dir/d.h"] =
+ guard("namespace ns { struct Bar { void d(); }; }");
+
+ TU.AdditionalFiles["system/e.h"] = guard("#include <f.h>");
+ TU.AdditionalFiles["system/f.h"] = guard("void f();");
+ TU.ExtraArgs.push_back("-isystem" + testPath("system"));
+
+ TU.AdditionalFiles["fuzz.h"] = guard("#include \"buzz.h\"");
+ TU.AdditionalFiles["buzz.h"] = guard("void buzz();");
+
+ TU.AdditionalFiles["baz.h"] = guard("#include \"private.h\"");
+ TU.AdditionalFiles["private.h"] = guard(R"cpp(
+ // IWYU pragma: private, include "public.h"
+ void foobar();
+ )cpp");
+ TU.AdditionalFiles["header.h"] = guard(R"cpp(
+ namespace std { class vector {}; }
+ )cpp");
+
+ TU.Code = MainFile.code();
+ ParsedAST AST = TU.build();
+
+ std::vector<clangd::Diag> Diags =
+ issueIncludeCleanerDiagnostics(AST, TU.Code);
+ EXPECT_THAT(
+ Diags,
+ UnorderedElementsAre(
+ AllOf(Diag(MainFile.range("b"),
+ "No header providing \"b\" is directly included"),
+ withFix(Fix(MainFile.range("insert_b"), "#include \"b.h\"\n",
+ "#include \"b.h\""))),
+ AllOf(Diag(MainFile.range("bar"),
+ "No header providing \"ns::Bar\" is directly included"),
+ withFix(Fix(MainFile.range("insert_d"),
+ "#include \"dir/d.h\"\n", "#include \"dir/d.h\""))),
+ AllOf(Diag(MainFile.range("f"),
+ "No header providing \"f\" is directly included"),
+ withFix(Fix(MainFile.range("insert_f"), "#include <f.h>\n",
+ "#include <f.h>"))),
+ AllOf(
+ Diag(MainFile.range("foobar"),
+ "No header providing \"foobar\" is directly included"),
+ withFix(Fix(MainFile.range("insert_foobar"),
+ "#include \"public.h\"\n", "#include \"public.h\""))),
+ AllOf(
+ Diag(MainFile.range("vector"),
+ "No header providing \"std::vector\" is directly included"),
+ withFix(Fix(MainFile.range("insert_vector"),
+ "#include <vector>\n", "#include <vector>")))));
+}
+
TEST(IncludeCleaner, VirtualBuffers) {
TestTU TU;
TU.Code = R"cpp(
void foo() {}
)cpp");
Config Cfg;
- Cfg.Diagnostics.UnusedIncludes = Config::UnusedIncludesPolicy::Experiment;
+ Cfg.Diagnostics.UnusedIncludes = Config::IncludesPolicy::Experiment;
WithContextValue Ctx(Config::Key, std::move(Cfg));
ParsedAST AST = TU.build();
ReferencedFiles.User.contains(AST.getSourceManager().getMainFileID()));
EXPECT_THAT(AST.getDiagnostics(), llvm::ValueIs(IsEmpty()));
EXPECT_THAT(computeUnusedIncludes(AST), IsEmpty());
- EXPECT_THAT(computeUnusedIncludesExperimental(AST), IsEmpty());
+ IncludeCleanerFindings Findings = computeIncludeCleanerFindings(AST);
+ EXPECT_THAT(Findings.UnusedIncludes, IsEmpty());
}
TEST(IncludeCleaner, RecursiveInclusion) {
EXPECT_THAT(AST.getDiagnostics(), llvm::ValueIs(IsEmpty()));
EXPECT_THAT(computeUnusedIncludes(AST), IsEmpty());
- EXPECT_THAT(computeUnusedIncludesExperimental(AST), IsEmpty());
+ IncludeCleanerFindings Findings = computeIncludeCleanerFindings(AST);
+ EXPECT_THAT(Findings.UnusedIncludes, IsEmpty());
}
TEST(IncludeCleaner, IWYUPragmaExport) {
// FIXME: This is not correct: foo.h is unused but is not diagnosed as such
// because we ignore headers with IWYU export pragmas for now.
EXPECT_THAT(computeUnusedIncludes(AST), IsEmpty());
- EXPECT_THAT(computeUnusedIncludesExperimental(AST), IsEmpty());
+ IncludeCleanerFindings Findings = computeIncludeCleanerFindings(AST);
+ EXPECT_THAT(Findings.UnusedIncludes, IsEmpty());
}
TEST(IncludeCleaner, NoDiagsForObjC) {
TU.ExtraArgs.emplace_back("-xobjective-c");
Config Cfg;
- Cfg.Diagnostics.UnusedIncludes = Config::UnusedIncludesPolicy::Strict;
+
+ Cfg.Diagnostics.UnusedIncludes = Config::IncludesPolicy::Strict;
+ Cfg.Diagnostics.MissingIncludes = Config::IncludesPolicy::Strict;
WithContextValue Ctx(Config::Key, std::move(Cfg));
ParsedAST AST = TU.build();
EXPECT_THAT(AST.getDiagnostics(), llvm::ValueIs(IsEmpty()));