diff --git a/clang-tools-extra/clangd/Config.h b/clang-tools-extra/clangd/Config.h --- a/clang-tools-extra/clangd/Config.h +++ b/clang-tools-extra/clangd/Config.h @@ -88,11 +88,12 @@ bool StandardLibrary = true; } Index; - enum class UnusedIncludesPolicy { - /// Diagnose unused includes. + enum class IncludesPolicy { + /// Diagnose missing and unused includes. Strict, None, - /// The same as Strict, but using the include-cleaner library. + /// The same as Strict, but using the include-cleaner library for + /// unused includes. Experiment, }; /// Controls warnings and errors when parsing code. @@ -107,11 +108,12 @@ llvm::StringMap CheckOptions; } ClangTidy; - UnusedIncludesPolicy UnusedIncludes = UnusedIncludesPolicy::None; - /// Enable emitting diagnostics using stale preambles. bool AllowStalePreamble = false; + IncludesPolicy UnusedIncludes = IncludesPolicy::None; + IncludesPolicy MissingIncludes = IncludesPolicy::None; + /// IncludeCleaner will not diagnose usages of these headers matched by /// these regexes. struct { diff --git a/clang-tools-extra/clangd/ConfigCompile.cpp b/clang-tools-extra/clangd/ConfigCompile.cpp --- a/clang-tools-extra/clangd/ConfigCompile.cpp +++ b/clang-tools-extra/clangd/ConfigCompile.cpp @@ -431,16 +431,16 @@ }); if (F.UnusedIncludes) - if (auto Val = - compileEnum("UnusedIncludes", - **F.UnusedIncludes) - .map("Strict", Config::UnusedIncludesPolicy::Strict) - .map("Experiment", Config::UnusedIncludesPolicy::Experiment) - .map("None", Config::UnusedIncludesPolicy::None) - .value()) + if (auto Val = compileEnum("UnusedIncludes", + **F.UnusedIncludes) + .map("Strict", Config::IncludesPolicy::Strict) + .map("Experiment", Config::IncludesPolicy::Experiment) + .map("None", Config::IncludesPolicy::None) + .value()) Out.Apply.push_back([Val](const Params &, Config &C) { C.Diagnostics.UnusedIncludes = *Val; }); + if (F.AllowStalePreamble) { if (auto Val = F.AllowStalePreamble) Out.Apply.push_back([Val](const Params &, Config &C) { @@ -448,6 +448,16 @@ }); } + if (F.MissingIncludes) + if (auto Val = compileEnum("MissingIncludes", + **F.MissingIncludes) + .map("Strict", Config::IncludesPolicy::Strict) + .map("None", Config::IncludesPolicy::None) + .value()) + Out.Apply.push_back([Val](const Params &, Config &C) { + C.Diagnostics.MissingIncludes = *Val; + }); + compile(std::move(F.Includes)); compile(std::move(F.ClangTidy)); } diff --git a/clang-tools-extra/clangd/ConfigFragment.h b/clang-tools-extra/clangd/ConfigFragment.h --- a/clang-tools-extra/clangd/ConfigFragment.h +++ b/clang-tools-extra/clangd/ConfigFragment.h @@ -221,7 +221,7 @@ /// This often has other advantages, such as skipping some analysis. std::vector> Suppress; - /// Controls how clangd will correct "unnecessary #include directives. + /// Controls how clangd will correct "unnecessary" #include directives. /// clangd can warn if a header is `#include`d but not used, and suggest /// removing it. // @@ -236,9 +236,23 @@ /// - None std::optional> UnusedIncludes; + /// Enable emitting diagnostics using stale preambles. std::optional> AllowStalePreamble; + /// Controls if clangd should analyze missing #include directives. + /// clangd will warn if no header providing a symbol is `#include`d + /// (missing) directly, and suggest adding it. + /// + /// Strict means a header providing a symbol is missing if it is not + /// *directly #include'd. The file might still compile if the header is + /// included transitively. + /// + /// Valid values are: + /// - Strict + /// - None + std::optional> MissingIncludes; + /// Controls IncludeCleaner diagnostics. struct IncludesBlock { /// Regexes that will be used to avoid diagnosing certain includes as diff --git a/clang-tools-extra/clangd/ConfigYAML.cpp b/clang-tools-extra/clangd/ConfigYAML.cpp --- a/clang-tools-extra/clangd/ConfigYAML.cpp +++ b/clang-tools-extra/clangd/ConfigYAML.cpp @@ -128,6 +128,9 @@ Dict.handle("UnusedIncludes", [&](Node &N) { F.UnusedIncludes = scalarValue(N, "UnusedIncludes"); }); + Dict.handle("MissingIncludes", [&](Node &N) { + F.MissingIncludes = scalarValue(N, "MissingIncludes"); + }); Dict.handle("Includes", [&](Node &N) { parse(F.Includes, N); }); Dict.handle("ClangTidy", [&](Node &N) { parse(F.ClangTidy, N); }); Dict.handle("AllowStalePreamble", [&](Node &N) { diff --git a/clang-tools-extra/clangd/IncludeCleaner.h b/clang-tools-extra/clangd/IncludeCleaner.h --- a/clang-tools-extra/clangd/IncludeCleaner.h +++ b/clang-tools-extra/clangd/IncludeCleaner.h @@ -20,18 +20,38 @@ #include "Headers.h" #include "ParsedAST.h" +#include "clang-include-cleaner/Types.h" #include "index/CanonicalIncludes.h" #include "clang/Basic/SourceLocation.h" #include "clang/Tooling/Inclusions/StandardLibrary.h" +#include "clang/Tooling/Syntax/Tokens.h" #include "llvm/ADT/DenseSet.h" #include "llvm/ADT/STLFunctionalExtras.h" #include "llvm/ADT/StringSet.h" #include +#include #include namespace clang { namespace clangd { +// Data needed for missing include diagnostics. +struct MissingIncludeDiagInfo { + include_cleaner::Symbol Symbol; + syntax::FileRange SymRefRange; + std::vector Providers; + + bool operator==(const MissingIncludeDiagInfo &Other) const { + return std::tie(SymRefRange, Providers, Symbol) == + std::tie(Other.SymRefRange, Other.Providers, Other.Symbol); + } +}; + +struct IncludeCleanerFindings { + std::vector UnusedIncludes; + std::vector MissingIncludes; +}; + struct ReferencedLocations { llvm::DenseSet User; llvm::DenseSet Stdlib; @@ -96,13 +116,10 @@ const llvm::DenseSet &ReferencedFiles, const llvm::StringSet<> &ReferencedPublicHeaders); +IncludeCleanerFindings computeIncludeCleanerFindings(ParsedAST &AST); std::vector computeUnusedIncludes(ParsedAST &AST); -// The same as computeUnusedIncludes, but it is an experimental and -// include-cleaner-lib-based implementation. -std::vector -computeUnusedIncludesExperimental(ParsedAST &AST); -std::vector issueUnusedIncludesDiagnostics(ParsedAST &AST, +std::vector issueIncludeCleanerDiagnostics(ParsedAST &AST, llvm::StringRef Code); /// Affects whether standard library includes should be considered for diff --git a/clang-tools-extra/clangd/IncludeCleaner.cpp b/clang-tools-extra/clangd/IncludeCleaner.cpp --- a/clang-tools-extra/clangd/IncludeCleaner.cpp +++ b/clang-tools-extra/clangd/IncludeCleaner.cpp @@ -8,32 +8,55 @@ #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 +#include #include +#include +#include namespace clang { namespace clangd { @@ -258,6 +281,17 @@ } } +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) @@ -288,14 +322,10 @@ 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; } @@ -325,6 +355,195 @@ return ID; } +include_cleaner::Includes +convertIncludes(const SourceManager &SM, + const llvm::ArrayRef 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 +collectMacroReferences(ParsedAST &AST) { + const auto &SM = AST.getSourceManager(); + // FIXME: !!this is a hacky way to collect macro references. + std::vector 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(&Sym.declaration()) + ->getQualifiedNameAsString(); + } + llvm_unreachable("Unknown symbol kind"); +} + +std::vector generateMissingIncludeDiagnostics( + ParsedAST &AST, llvm::ArrayRef MissingIncludes, + llvm::StringRef Code) { + std::vector 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 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 generateUnusedIncludeDiagnostics( + PathRef FileName, llvm::ArrayRef UnusedIncludes, + llvm::StringRef Code) { + std::vector 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, @@ -474,105 +693,85 @@ translateToHeaderIDs(ReferencedFiles, AST.getIncludeStructure(), SM); return getUnused(AST, ReferencedHeaders, ReferencedFiles.SpelledUmbrellas); } -std::vector -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 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 Macros = + collectMacroReferences(AST); + std::vector MissingIncludes; llvm::DenseSet Used; + trace::Span Tracer("include_cleaner::walkUsed"); include_cleaner::walkUsed( AST.getLocalTopLevelDecls(), /*MacroRefs=*/Macros, AST.getPragmaIncludes(), SM, [&](const include_cleaner::SymbolReference &Ref, llvm::ArrayRef 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(*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 UnusedIncludes = + getUnused(AST, Used, /*ReferencedPublicHeaders*/ {}); + return {std::move(UnusedIncludes), std::move(MissingIncludes)}; } -std::vector issueUnusedIncludesDiagnostics(ParsedAST &AST, +std::vector 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 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 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; } diff --git a/clang-tools-extra/clangd/ParsedAST.cpp b/clang-tools-extra/clangd/ParsedAST.cpp --- a/clang-tools-extra/clangd/ParsedAST.cpp +++ b/clang-tools-extra/clangd/ParsedAST.cpp @@ -687,13 +687,9 @@ std::move(Macros), std::move(Marks), std::move(ParsedDecls), std::move(Diags), std::move(Includes), std::move(CanonIncludes)); - if (Result.Diags) { - auto UnusedHeadersDiags = - issueUnusedIncludesDiagnostics(Result, Inputs.Contents); - Result.Diags->insert(Result.Diags->end(), - make_move_iterator(UnusedHeadersDiags.begin()), - make_move_iterator(UnusedHeadersDiags.end())); - } + if (Result.Diags) + llvm::move(issueIncludeCleanerDiagnostics(Result, Inputs.Contents), + std::back_inserter(*Result.Diags)); return std::move(Result); } diff --git a/clang-tools-extra/clangd/Preamble.cpp b/clang-tools-extra/clangd/Preamble.cpp --- a/clang-tools-extra/clangd/Preamble.cpp +++ b/clang-tools-extra/clangd/Preamble.cpp @@ -128,7 +128,9 @@ SourceMgr = &CI.getSourceManager(); Includes.collect(CI); if (Config::current().Diagnostics.UnusedIncludes == - Config::UnusedIncludesPolicy::Experiment) + Config::IncludesPolicy::Experiment || + Config::current().Diagnostics.MissingIncludes == + Config::IncludesPolicy::Strict) Pragmas.record(CI); if (BeforeExecuteCallback) BeforeExecuteCallback(CI); diff --git a/clang-tools-extra/clangd/unittests/ConfigCompileTests.cpp b/clang-tools-extra/clangd/unittests/ConfigCompileTests.cpp --- a/clang-tools-extra/clangd/unittests/ConfigCompileTests.cpp +++ b/clang-tools-extra/clangd/unittests/ConfigCompileTests.cpp @@ -249,19 +249,19 @@ // Defaults to None. EXPECT_TRUE(compileAndApply()); EXPECT_EQ(Conf.Diagnostics.UnusedIncludes, - Config::UnusedIncludesPolicy::None); + Config::IncludesPolicy::None); Frag = {}; Frag.Diagnostics.UnusedIncludes.emplace("None"); EXPECT_TRUE(compileAndApply()); EXPECT_EQ(Conf.Diagnostics.UnusedIncludes, - Config::UnusedIncludesPolicy::None); + Config::IncludesPolicy::None); Frag = {}; Frag.Diagnostics.UnusedIncludes.emplace("Strict"); EXPECT_TRUE(compileAndApply()); EXPECT_EQ(Conf.Diagnostics.UnusedIncludes, - Config::UnusedIncludesPolicy::Strict); + Config::IncludesPolicy::Strict); Frag = {}; EXPECT_TRUE(Conf.Diagnostics.Includes.IgnoreHeader.empty()) diff --git a/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp b/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp --- a/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp +++ b/clang-tools-extra/clangd/unittests/DiagnosticsTests.cpp @@ -1900,7 +1900,7 @@ // Off by default. EXPECT_THAT(*TU.build().getDiagnostics(), IsEmpty()); Config Cfg; - Cfg.Diagnostics.UnusedIncludes = Config::UnusedIncludesPolicy::Strict; + Cfg.Diagnostics.UnusedIncludes = Config::IncludesPolicy::Strict; // Set filtering. Cfg.Diagnostics.Includes.IgnoreHeader.emplace_back( [](llvm::StringRef Header) { return Header.endswith("ignore.h"); }); diff --git a/clang-tools-extra/clangd/unittests/IncludeCleanerTests.cpp b/clang-tools-extra/clangd/unittests/IncludeCleanerTests.cpp --- a/clang-tools-extra/clangd/unittests/IncludeCleanerTests.cpp +++ b/clang-tools-extra/clangd/unittests/IncludeCleanerTests.cpp @@ -8,26 +8,58 @@ #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 +#include +#include 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 withFix(::testing::Matcher 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(); } @@ -342,7 +374,8 @@ auto AST = TU.build(); EXPECT_THAT(computeUnusedIncludes(AST), ElementsAre(Pointee(writtenInclusion("")))); - EXPECT_THAT(computeUnusedIncludesExperimental(AST), + IncludeCleanerFindings Findings = computeIncludeCleanerFindings(AST); + EXPECT_THAT(Findings.UnusedIncludes, ElementsAre(Pointee(writtenInclusion("")))); } @@ -379,12 +412,138 @@ 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(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(Start), + static_cast(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.ends_with("buzz.h"); }}; + 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 +$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 "); + 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 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 \n", + "#include "))), + 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 \n", "#include "))))); +} + TEST(IncludeCleaner, VirtualBuffers) { TestTU TU; TU.Code = R"cpp( @@ -538,7 +697,7 @@ 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(); @@ -554,7 +713,8 @@ 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) { @@ -583,7 +743,8 @@ 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) { @@ -608,7 +769,8 @@ // 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) { @@ -627,7 +789,9 @@ 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())); diff --git a/clang-tools-extra/clangd/unittests/PreambleTests.cpp b/clang-tools-extra/clangd/unittests/PreambleTests.cpp --- a/clang-tools-extra/clangd/unittests/PreambleTests.cpp +++ b/clang-tools-extra/clangd/unittests/PreambleTests.cpp @@ -665,7 +665,7 @@ TEST(PreamblePatch, DiagnosticsToPreamble) { Config Cfg; Cfg.Diagnostics.AllowStalePreamble = true; - Cfg.Diagnostics.UnusedIncludes = Config::UnusedIncludesPolicy::Strict; + Cfg.Diagnostics.UnusedIncludes = Config::IncludesPolicy::Strict; WithContextValue WithCfg(Config::Key, std::move(Cfg)); llvm::StringMap AdditionalFiles; diff --git a/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Analysis.h b/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Analysis.h --- a/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Analysis.h +++ b/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Analysis.h @@ -73,6 +73,8 @@ std::string fixIncludes(const AnalysisResults &Results, llvm::StringRef Code, const format::FormatStyle &IncludeStyle); +std::string spellHeader(const Header &H, HeaderSearch &HS, + const FileEntry *Main); } // namespace include_cleaner } // namespace clang diff --git a/clang-tools-extra/include-cleaner/lib/Analysis.cpp b/clang-tools-extra/include-cleaner/lib/Analysis.cpp --- a/clang-tools-extra/include-cleaner/lib/Analysis.cpp +++ b/clang-tools-extra/include-cleaner/lib/Analysis.cpp @@ -49,8 +49,8 @@ } } -static std::string spellHeader(const Header &H, HeaderSearch &HS, - const FileEntry *Main) { +std::string spellHeader(const Header &H, HeaderSearch &HS, + const FileEntry *Main) { switch (H.kind()) { case Header::Physical: { bool IsSystem = false;