diff --git a/clang-tools-extra/clang-tidy/misc/CMakeLists.txt b/clang-tools-extra/clang-tidy/misc/CMakeLists.txt --- a/clang-tools-extra/clang-tidy/misc/CMakeLists.txt +++ b/clang-tools-extra/clang-tidy/misc/CMakeLists.txt @@ -7,6 +7,7 @@ add_subdirectory(ConfusableTable) +include_directories(BEFORE "${CMAKE_CURRENT_SOURCE_DIR}/../../include-cleaner/include") add_custom_command( OUTPUT Confusables.inc @@ -19,6 +20,7 @@ ConstCorrectnessCheck.cpp DefinitionsInHeadersCheck.cpp ConfusableIdentifierCheck.cpp + IncludeCleanerCheck.cpp MiscTidyModule.cpp MisleadingBidirectional.cpp MisleadingIdentifier.cpp @@ -53,6 +55,7 @@ clangAST clangASTMatchers clangBasic + clangIncludeCleaner clangLex clangSerialization clangTooling diff --git a/clang-tools-extra/clang-tidy/misc/IncludeCleanerCheck.h b/clang-tools-extra/clang-tidy/misc/IncludeCleanerCheck.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clang-tidy/misc/IncludeCleanerCheck.h @@ -0,0 +1,53 @@ +//===--- IncludeCleanerCheck.h - clang-tidy ---------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MISC_INCLUDECLEANER_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MISC_INCLUDECLEANER_H + +#include "../ClangTidyCheck.h" +#include "../ClangTidyDiagnosticConsumer.h" +#include "../ClangTidyOptions.h" +#include "clang-include-cleaner/Record.h" +#include "clang-include-cleaner/Types.h" +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "clang/Basic/LLVM.h" +#include "clang/Basic/SourceLocation.h" +#include "clang/Lex/HeaderSearch.h" +#include "llvm/Support/Regex.h" +#include + +namespace clang::tidy::misc { + +/// Checks for unused and missing includes. Generates findings only for +/// the main file of a translation unit. +/// Findings correspond to https://clangd.llvm.org/design/include-cleaner. +/// +/// For the user-facing documentation see: +/// http://clang.llvm.org/extra/clang-tidy/checks/misc/include-cleaner.html +class IncludeCleanerCheck : public ClangTidyCheck { +public: + IncludeCleanerCheck(StringRef Name, ClangTidyContext *Context); + void registerMatchers(ast_matchers::MatchFinder *Finder) override; + void check(const ast_matchers::MatchFinder::MatchResult &Result) override; + void registerPPCallbacks(const SourceManager &SM, Preprocessor *PP, + Preprocessor *ModuleExpanderPP) override; + void storeOptions(ClangTidyOptions::OptionMap &Opts) override; + bool isLanguageVersionSupported(const LangOptions &LangOpts) const override; + +private: + include_cleaner::RecordedPP RecordedPreprocessor; + include_cleaner::PragmaIncludes RecordedPI; + HeaderSearch *HS; + std::vector IgnoreHeaders; + llvm::SmallVector IgnoreHeadersRegex; + bool shouldIgnore(const include_cleaner::Header &H); +}; + +} // namespace clang::tidy::misc + +#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MISC_INCLUDECLEANER_H diff --git a/clang-tools-extra/clang-tidy/misc/IncludeCleanerCheck.cpp b/clang-tools-extra/clang-tidy/misc/IncludeCleanerCheck.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clang-tidy/misc/IncludeCleanerCheck.cpp @@ -0,0 +1,202 @@ +//===--- IncludeCleanerCheck.cpp - clang-tidy -----------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "IncludeCleanerCheck.h" +#include "../ClangTidyCheck.h" +#include "../ClangTidyDiagnosticConsumer.h" +#include "../ClangTidyOptions.h" +#include "../utils/OptionsUtils.h" +#include "clang-include-cleaner/Analysis.h" +#include "clang-include-cleaner/Record.h" +#include "clang-include-cleaner/Types.h" +#include "clang/AST/ASTContext.h" +#include "clang/AST/Decl.h" +#include "clang/AST/DeclBase.h" +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "clang/ASTMatchers/ASTMatchers.h" +#include "clang/Basic/Diagnostic.h" +#include "clang/Basic/FileEntry.h" +#include "clang/Basic/LLVM.h" +#include "clang/Basic/LangOptions.h" +#include "clang/Basic/SourceLocation.h" +#include "clang/Format/Format.h" +#include "clang/Lex/Preprocessor.h" +#include "clang/Tooling/Core/Replacement.h" +#include "clang/Tooling/Inclusions/HeaderIncludes.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/Regex.h" +#include +#include +#include + +using namespace clang::ast_matchers; + +namespace clang::tidy::misc { + +namespace { +struct MissingIncludeInfo { + SourceLocation SymRefLocation; + include_cleaner::Header Missing; +}; +} // namespace + +IncludeCleanerCheck::IncludeCleanerCheck(StringRef Name, + ClangTidyContext *Context) + : ClangTidyCheck(Name, Context), + IgnoreHeaders(utils::options::parseStringList( + Options.getLocalOrGlobal("IgnoreHeaders", ""))) { + for (const auto &Header : IgnoreHeaders) { + if (!llvm::Regex{Header}.isValid()) + configurationDiag("Invalid ignore headers regex '%0'") << Header; + std::string HeaderSuffix{Header.str()}; + if (!Header.ends_with("$")) + HeaderSuffix += "$"; + IgnoreHeadersRegex.emplace_back(HeaderSuffix); + } +} + +void IncludeCleanerCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) { + Options.store(Opts, "IgnoreHeaders", + utils::options::serializeStringList(IgnoreHeaders)); +} + +bool IncludeCleanerCheck::isLanguageVersionSupported( + const LangOptions &LangOpts) const { + return !LangOpts.ObjC; +} + +void IncludeCleanerCheck::registerMatchers(MatchFinder *Finder) { + Finder->addMatcher(translationUnitDecl().bind("top"), this); +} + +void IncludeCleanerCheck::registerPPCallbacks(const SourceManager &SM, + Preprocessor *PP, + Preprocessor *ModuleExpanderPP) { + PP->addPPCallbacks(RecordedPreprocessor.record(*PP)); + HS = &PP->getHeaderSearchInfo(); + RecordedPI.record(*PP); +} + +bool IncludeCleanerCheck::shouldIgnore(const include_cleaner::Header &H) { + return llvm::any_of(IgnoreHeadersRegex, [&H](const llvm::Regex &R) { + switch (H.kind()) { + case include_cleaner::Header::Standard: + return R.match(H.standard().name()); + case include_cleaner::Header::Verbatim: + return R.match(H.verbatim()); + case include_cleaner::Header::Physical: + return R.match(H.physical()->tryGetRealPathName()); + } + llvm_unreachable("Unknown Header kind."); + }); +} + +void IncludeCleanerCheck::check(const MatchFinder::MatchResult &Result) { + const SourceManager *SM = Result.SourceManager; + const FileEntry *MainFile = SM->getFileEntryForID(SM->getMainFileID()); + llvm::DenseSet Used; + std::vector Missing; + llvm::SmallVector MainFileDecls; + for (Decl *D : Result.Nodes.getNodeAs("top")->decls()) { + if (!SM->isWrittenInMainFile(SM->getExpansionLoc(D->getLocation()))) + continue; + // FIXME: Filter out implicit template specializations. + MainFileDecls.push_back(D); + } + // FIXME: Find a way to have less code duplication between include-cleaner + // analysis implementation and the below code. + walkUsed(MainFileDecls, RecordedPreprocessor.MacroReferences, &RecordedPI, + *SM, + [&](const include_cleaner::SymbolReference &Ref, + llvm::ArrayRef Providers) { + bool Satisfied = false; + for (const include_cleaner::Header &H : Providers) { + if (H.kind() == include_cleaner::Header::Physical && + H.physical() == MainFile) + Satisfied = true; + + for (const include_cleaner::Include *I : + RecordedPreprocessor.Includes.match(H)) { + Used.insert(I); + Satisfied = true; + } + } + if (!Satisfied && !Providers.empty() && + Ref.RT == include_cleaner::RefType::Explicit && + !shouldIgnore(Providers.front())) + Missing.push_back({Ref.RefLocation, Providers.front()}); + }); + + std::vector Unused; + for (const include_cleaner::Include &I : + RecordedPreprocessor.Includes.all()) { + if (Used.contains(&I) || !I.Resolved) + continue; + if (RecordedPI.shouldKeep(I.Line)) + continue; + // Check if main file is the public interface for a private header. If so + // we shouldn't diagnose it as unused. + if (auto PHeader = RecordedPI.getPublic(I.Resolved); !PHeader.empty()) { + PHeader = PHeader.trim("<>\""); + // Since most private -> public mappings happen in a verbatim way, we + // check textually here. This might go wrong in presence of symlinks or + // header mappings. But that's not different than rest of the places. + if (getCurrentMainFile().endswith(PHeader)) + continue; + } + + if (llvm::none_of(IgnoreHeadersRegex, + [Resolved = I.Resolved->tryGetRealPathName()]( + const llvm::Regex &R) { return R.match(Resolved); })) + Unused.push_back(&I); + } + + llvm::StringRef Code = SM->getBufferData(SM->getMainFileID()); + auto FileStyle = + format::getStyle(format::DefaultFormatStyle, getCurrentMainFile(), + format::DefaultFallbackStyle, Code, + &SM->getFileManager().getVirtualFileSystem()); + if (!FileStyle) + FileStyle = format::getLLVMStyle(); + + for (const auto *Inc : Unused) { + diag(Inc->HashLocation, "included header %0 is not used directly") + << Inc->quote() + << FixItHint::CreateRemoval(CharSourceRange::getCharRange( + SM->translateLineCol(SM->getMainFileID(), Inc->Line, 1), + SM->translateLineCol(SM->getMainFileID(), Inc->Line + 1, 1))); + } + + tooling::HeaderIncludes HeaderIncludes(getCurrentMainFile(), Code, + FileStyle->IncludeStyle); + for (const auto &Inc : Missing) { + std::string Spelling = + include_cleaner::spellHeader(Inc.Missing, *HS, MainFile); + bool Angled = llvm::StringRef{Spelling}.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. + if (auto Replacement = + HeaderIncludes.insert(llvm::StringRef{Spelling}.trim("\"<>"), + Angled, tooling::IncludeDirective::Include)) + diag(SM->getSpellingLoc(Inc.SymRefLocation), + "no header providing %0 is directly included") + << Spelling + << FixItHint::CreateInsertion( + SM->getComposedLoc(SM->getMainFileID(), + Replacement->getOffset()), + Replacement->getReplacementText()); + } +} + +} // namespace clang::tidy::misc diff --git a/clang-tools-extra/clang-tidy/misc/MiscTidyModule.cpp b/clang-tools-extra/clang-tidy/misc/MiscTidyModule.cpp --- a/clang-tools-extra/clang-tidy/misc/MiscTidyModule.cpp +++ b/clang-tools-extra/clang-tidy/misc/MiscTidyModule.cpp @@ -12,6 +12,7 @@ #include "ConfusableIdentifierCheck.h" #include "ConstCorrectnessCheck.h" #include "DefinitionsInHeadersCheck.h" +#include "IncludeCleanerCheck.h" #include "MisleadingBidirectional.h" #include "MisleadingIdentifier.h" #include "MisplacedConstCheck.h" @@ -41,6 +42,7 @@ "misc-const-correctness"); CheckFactories.registerCheck( "misc-definitions-in-headers"); + CheckFactories.registerCheck("misc-include-cleaner"); CheckFactories.registerCheck( "misc-misleading-bidirectional"); CheckFactories.registerCheck( diff --git a/clang-tools-extra/clangd/TidyProvider.cpp b/clang-tools-extra/clangd/TidyProvider.cpp --- a/clang-tools-extra/clangd/TidyProvider.cpp +++ b/clang-tools-extra/clangd/TidyProvider.cpp @@ -196,32 +196,35 @@ TidyProvider disableUnusableChecks(llvm::ArrayRef ExtraBadChecks) { constexpr llvm::StringLiteral Seperator(","); - static const std::string BadChecks = - llvm::join_items(Seperator, - // We want this list to start with a seperator to - // simplify appending in the lambda. So including an - // empty string here will force that. - "", - // ----- False Positives ----- - - // Check relies on seeing ifndef/define/endif directives, - // clangd doesn't replay those when using a preamble. - "-llvm-header-guard", "-modernize-macro-to-enum", - - // ----- Crashing Checks ----- - - // Check can choke on invalid (intermediate) c++ - // code, which is often the case when clangd - // tries to build an AST. - "-bugprone-use-after-move", - // Alias for bugprone-use-after-move. - "-hicpp-invalid-access-moved", - - // ----- Performance problems ----- - - // This check runs expensive analysis for each variable. - // It has been observed to increase reparse time by 10x. - "-misc-const-correctness"); + static const std::string BadChecks = llvm::join_items( + Seperator, + // We want this list to start with a seperator to + // simplify appending in the lambda. So including an + // empty string here will force that. + "", + // include-cleaner is directly integrated in IncludeCleaner.cpp + "-misc-include-cleaner", + + // ----- False Positives ----- + + // Check relies on seeing ifndef/define/endif directives, + // clangd doesn't replay those when using a preamble. + "-llvm-header-guard", "-modernize-macro-to-enum", + + // ----- Crashing Checks ----- + + // Check can choke on invalid (intermediate) c++ + // code, which is often the case when clangd + // tries to build an AST. + "-bugprone-use-after-move", + // Alias for bugprone-use-after-move. + "-hicpp-invalid-access-moved", + + // ----- Performance problems ----- + + // This check runs expensive analysis for each variable. + // It has been observed to increase reparse time by 10x. + "-misc-const-correctness"); size_t Size = BadChecks.size(); for (const std::string &Str : ExtraBadChecks) { diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst --- a/clang-tools-extra/docs/ReleaseNotes.rst +++ b/clang-tools-extra/docs/ReleaseNotes.rst @@ -165,6 +165,11 @@ Checks that all implicit and explicit inline functions in header files are tagged with the ``LIBC_INLINE`` macro. +- New :doc:`misc-include-cleaner + ` check. + + Checks for unused and missing includes. + - New :doc:`modernize-type-traits ` check. diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst b/clang-tools-extra/docs/clang-tidy/checks/list.rst --- a/clang-tools-extra/docs/clang-tidy/checks/list.rst +++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst @@ -255,6 +255,7 @@ `misc-confusable-identifiers `_, `misc-const-correctness `_, "Yes" `misc-definitions-in-headers `_, "Yes" + `misc-include-cleaner `_, "Yes" `misc-misleading-bidirectional `_, `misc-misleading-identifier `_, `misc-misplaced-const `_, diff --git a/clang-tools-extra/docs/clang-tidy/checks/misc/include-cleaner.rst b/clang-tools-extra/docs/clang-tidy/checks/misc/include-cleaner.rst new file mode 100644 --- /dev/null +++ b/clang-tools-extra/docs/clang-tidy/checks/misc/include-cleaner.rst @@ -0,0 +1,34 @@ +.. title:: clang-tidy - misc-include-cleaner + +misc-include-cleaner +==================== + +Checks for unused and missing includes. Generates findings only for +the main file of a translation unit. +Findings correspond to https://clangd.llvm.org/design/include-cleaner. + +Example: + +.. code-block:: c++ + // foo.h + class Foo{}; + // bar.h + #include "baz.h" + class Bar{}; + // baz.h + class Baz{}; + // main.cc + #include "bar.h" // OK: uses class Bar from bar.h + #include "foo.h" // warning: unused include "foo.h" + Bar bar; + Baz baz; // warning: missing include "baz.h" + +Options +------- + +.. option:: IgnoreHeaders + + A semicolon-separated list of regexes to disable insertion/removal of header + files that match this regex as a suffix. E.g., `foo/.*` disables + insertion/removal for all headers under the directory `foo`. By default, no + headers will be ignored. diff --git a/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h b/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h --- a/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h +++ b/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h @@ -18,6 +18,7 @@ #define CLANG_INCLUDE_CLEANER_RECORD_H #include "clang-include-cleaner/Types.h" +#include "clang/Basic/SourceLocation.h" #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/DenseSet.h" #include "llvm/ADT/SmallVector.h" @@ -52,6 +53,10 @@ /// to the structure. void record(const CompilerInstance &CI); + /// Installs an analysing PPCallback and CommentHandler and populates results + /// to the structure. + void record(Preprocessor &P); + /// Returns true if the given #include of the main-file should never be /// removed. bool shouldKeep(unsigned HashLineNumber) const { diff --git a/clang-tools-extra/include-cleaner/lib/Record.cpp b/clang-tools-extra/include-cleaner/lib/Record.cpp --- a/clang-tools-extra/include-cleaner/lib/Record.cpp +++ b/clang-tools-extra/include-cleaner/lib/Record.cpp @@ -19,6 +19,8 @@ #include "clang/Lex/Preprocessor.h" #include "clang/Tooling/Inclusions/HeaderAnalysis.h" #include "clang/Tooling/Inclusions/StandardLibrary.h" +#include +#include namespace clang::include_cleaner { namespace { @@ -148,8 +150,9 @@ class PragmaIncludes::RecordPragma : public PPCallbacks, public CommentHandler { public: RecordPragma(const CompilerInstance &CI, PragmaIncludes *Out) - : SM(CI.getSourceManager()), - HeaderInfo(CI.getPreprocessor().getHeaderSearchInfo()), Out(Out), + : RecordPragma(CI.getPreprocessor(), Out) {} + RecordPragma(const Preprocessor &P, PragmaIncludes *Out) + : SM(P.getSourceManager()), HeaderInfo(P.getHeaderSearchInfo()), Out(Out), UniqueStrings(Arena) {} void FileChanged(SourceLocation Loc, FileChangeReason Reason, @@ -342,6 +345,12 @@ CI.getPreprocessor().addPPCallbacks(std::move(Record)); } +void PragmaIncludes::record(Preprocessor &P) { + auto Record = std::make_unique(P, this); + P.addCommentHandler(Record.get()); + P.addPPCallbacks(std::move(Record)); +} + llvm::StringRef PragmaIncludes::getPublic(const FileEntry *F) const { auto It = IWYUPublic.find(F->getUniqueID()); if (It == IWYUPublic.end()) @@ -350,8 +359,8 @@ } static llvm::SmallVector -toFileEntries(llvm::ArrayRef FileNames, FileManager& FM) { - llvm::SmallVector Results; +toFileEntries(llvm::ArrayRef FileNames, FileManager &FM) { + llvm::SmallVector Results; for (auto FName : FileNames) { // FIMXE: log the failing cases? diff --git a/clang-tools-extra/test/clang-tidy/checkers/misc/Inputs/bar.h b/clang-tools-extra/test/clang-tidy/checkers/misc/Inputs/bar.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/misc/Inputs/bar.h @@ -0,0 +1,4 @@ +#pragma once +#include "baz.h" +#include "private.h" +int bar(); diff --git a/clang-tools-extra/test/clang-tidy/checkers/misc/Inputs/baz.h b/clang-tools-extra/test/clang-tidy/checkers/misc/Inputs/baz.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/misc/Inputs/baz.h @@ -0,0 +1,2 @@ +#pragma once +int baz(); diff --git a/clang-tools-extra/test/clang-tidy/checkers/misc/Inputs/foo.h b/clang-tools-extra/test/clang-tidy/checkers/misc/Inputs/foo.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/misc/Inputs/foo.h @@ -0,0 +1,2 @@ +#pragma once +void foo(); diff --git a/clang-tools-extra/test/clang-tidy/checkers/misc/Inputs/private.h b/clang-tools-extra/test/clang-tidy/checkers/misc/Inputs/private.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/misc/Inputs/private.h @@ -0,0 +1,2 @@ +// IWYU pragma: private, include "public.h" +int foobar(); diff --git a/clang-tools-extra/test/clang-tidy/checkers/misc/include-cleaner.cpp b/clang-tools-extra/test/clang-tidy/checkers/misc/include-cleaner.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/misc/include-cleaner.cpp @@ -0,0 +1,17 @@ +// RUN: %check_clang_tidy %s misc-include-cleaner %t -- -- -I%S/Inputs -isystem%S/system +#include "bar.h" +// CHECK-FIXES: {{^}}#include "baz.h"{{$}} +#include "foo.h" +// CHECK-MESSAGES: :[[@LINE-1]]:1: warning: included header "foo.h" is not used directly [misc-include-cleaner] +// CHECK-FIXES: {{^}} +// CHECK-FIXES: {{^}}#include {{$}} +#include +// CHECK-MESSAGES: :[[@LINE-1]]:1: warning: included header is not used directly [misc-include-cleaner] +// CHECK-FIXES: {{^}} +int BarResult = bar(); +int BazResult = baz(); +// CHECK-MESSAGES: :[[@LINE-1]]:17: warning: no header providing "baz.h" is directly included [misc-include-cleaner] +std::string HelloString; +// CHECK-MESSAGES: :[[@LINE-1]]:6: warning: no header providing is directly included [misc-include-cleaner] +int FooBarResult = foobar(); +// CHECK-MESSAGES: :[[@LINE-1]]:20: warning: no header providing "public.h" is directly included [misc-include-cleaner] diff --git a/clang-tools-extra/test/clang-tidy/checkers/misc/system/string.h b/clang-tools-extra/test/clang-tidy/checkers/misc/system/string.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/misc/system/string.h @@ -0,0 +1,2 @@ +#pragma once +namespace std { class string {}; } diff --git a/clang-tools-extra/test/clang-tidy/checkers/misc/system/vector.h b/clang-tools-extra/test/clang-tidy/checkers/misc/system/vector.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/misc/system/vector.h @@ -0,0 +1,4 @@ +#pragma once +#include + +namespace std { class vector {}; } diff --git a/clang-tools-extra/unittests/clang-tidy/CMakeLists.txt b/clang-tools-extra/unittests/clang-tidy/CMakeLists.txt --- a/clang-tools-extra/unittests/clang-tidy/CMakeLists.txt +++ b/clang-tools-extra/unittests/clang-tidy/CMakeLists.txt @@ -15,12 +15,14 @@ get_filename_component(CLANG_LINT_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../clang-tidy REALPATH) include_directories(${CLANG_LINT_SOURCE_DIR}) +include_directories(BEFORE "${CMAKE_CURRENT_SOURCE_DIR}/../../include-cleaner/include") add_extra_unittest(ClangTidyTests AddConstTest.cpp ClangTidyDiagnosticConsumerTest.cpp ClangTidyOptionsTest.cpp DeclRefExprUtilsTest.cpp + IncludeCleanerTest.cpp IncludeInserterTest.cpp GlobListTest.cpp GoogleModuleTest.cpp @@ -46,12 +48,14 @@ clangTooling clangToolingCore clangTransformer + clangIncludeCleaner ) target_link_libraries(ClangTidyTests PRIVATE clangTidy clangTidyAndroidModule clangTidyGoogleModule + clangTidyMiscModule clangTidyLLVMModule clangTidyModernizeModule clangTidyObjCModule diff --git a/clang-tools-extra/unittests/clang-tidy/IncludeCleanerTest.cpp b/clang-tools-extra/unittests/clang-tidy/IncludeCleanerTest.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/unittests/clang-tidy/IncludeCleanerTest.cpp @@ -0,0 +1,233 @@ +//===--- IncludeCleanerTest.cpp - clang-tidy -----------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "ClangTidyDiagnosticConsumer.h" +#include "ClangTidyOptions.h" +#include "ClangTidyTest.h" +#include "misc/IncludeCleanerCheck.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/Path.h" +#include "gtest/gtest.h" +#include + +#include +#include + +using namespace clang::tidy::misc; + +namespace clang { +namespace tidy { +namespace test { +namespace { + +std::string +appendPathFileSystemIndependent(std::initializer_list Segments) { + llvm::SmallString<32> Result; + for (const auto &Segment : Segments) + llvm::sys::path::append(Result, llvm::sys::path::Style::native, Segment); + return std::string(Result.str()); +} + +TEST(IncludeCleanerCheckTest, BasicUnusedIncludes) { + const char *PreCode = R"( +#include "bar.h" +#include +#include "bar.h" +)"; + const char *PostCode = "\n"; + + std::vector Errors; + EXPECT_EQ(PostCode, runCheckOnCode( + PreCode, &Errors, "file.cpp", std::nullopt, + ClangTidyOptions(), {{"bar.h", ""}, {"vector", ""}})); +} + +TEST(IncludeCleanerCheckTest, SuppressUnusedIncludes) { + const char *PreCode = R"( +#include "bar.h" +#include "foo/qux.h" +#include "baz/qux/qux.h" +#include +)"; + + const char *PostCode = R"( +#include "bar.h" +#include "foo/qux.h" +#include +)"; + + std::vector Errors; + ClangTidyOptions Opts; + Opts.CheckOptions["IgnoreHeaders"] = llvm::StringRef{llvm::formatv( + "bar.h;{0};{1};vector", appendPathFileSystemIndependent({"foo", "qux.h"}), + appendPathFileSystemIndependent({"baz", "qux"}))}; + EXPECT_EQ( + PostCode, + runCheckOnCode( + PreCode, &Errors, "file.cpp", std::nullopt, Opts, + {{"bar.h", ""}, + {"vector", ""}, + {appendPathFileSystemIndependent({"foo", "qux.h"}), ""}, + {appendPathFileSystemIndependent({"baz", "qux", "qux.h"}), ""}})); +} + +TEST(IncludeCleanerCheckTest, BasicMissingIncludes) { + const char *PreCode = R"( +#include "bar.h" + +int BarResult = bar(); +int BazResult = baz(); +)"; + const char *PostCode = R"( +#include "bar.h" +#include "baz.h" + +int BarResult = bar(); +int BazResult = baz(); +)"; + + std::vector Errors; + EXPECT_EQ(PostCode, + runCheckOnCode( + PreCode, &Errors, "file.cpp", std::nullopt, ClangTidyOptions(), + {{"bar.h", R"(#pragma once + #include "baz.h" + int bar(); + )"}, + {"baz.h", R"(#pragma once + int baz(); + )"}})); +} + +TEST(IncludeCleanerCheckTest, SuppressMissingIncludes) { + const char *PreCode = R"( +#include "bar.h" + +int BarResult = bar(); +int BazResult = baz(); +int QuxResult = qux(); +)"; + + ClangTidyOptions Opts; + Opts.CheckOptions["IgnoreHeaders"] = llvm::StringRef{ + "baz.h;" + appendPathFileSystemIndependent({"foo", "qux.h"})}; + std::vector Errors; + EXPECT_EQ(PreCode, runCheckOnCode( + PreCode, &Errors, "file.cpp", std::nullopt, Opts, + {{"bar.h", R"(#pragma once + #include "baz.h" + #include "foo/qux.h" + int bar(); + )"}, + {"baz.h", R"(#pragma once + int baz(); + )"}, + {"foo/qux.h", + R"(#pragma once + int qux(); + )"}})); +} + +TEST(IncludeCleanerCheckTest, SystemMissingIncludes) { + const char *PreCode = R"( +#include + +std::string HelloString; +std::vector Vec; +)"; + const char *PostCode = R"( +#include +#include + +std::string HelloString; +std::vector Vec; +)"; + + std::vector Errors; + EXPECT_EQ(PostCode, + runCheckOnCode( + PreCode, &Errors, "file.cpp", std::nullopt, ClangTidyOptions(), + {{"string", R"(#pragma once + namespace std { class string {}; } + )"}, + {"vector", R"(#pragma once + #include + namespace std { class vector {}; } + )"}})); +} + +TEST(IncludeCleanerCheckTest, PragmaMissingIncludes) { + const char *PreCode = R"( +#include "bar.h" + +int BarResult = bar(); +int FooBarResult = foobar(); +)"; + const char *PostCode = R"( +#include "bar.h" +#include "public.h" + +int BarResult = bar(); +int FooBarResult = foobar(); +)"; + + std::vector Errors; + EXPECT_EQ(PostCode, + runCheckOnCode( + PreCode, &Errors, "file.cpp", std::nullopt, ClangTidyOptions(), + {{"bar.h", R"(#pragma once + #include "private.h" + int bar(); + )"}, + {"private.h", R"(#pragma once + // IWYU pragma: private, include "public.h" + int foobar(); + )"}})); +} + +TEST(IncludeCleanerCheckTest, DeclFromMacroExpansion) { + const char *PreCode = R"( +#include "foo.h" + +DECLARE(myfunc) { + int a; +} +)"; + + std::vector Errors; + EXPECT_EQ(PreCode, + runCheckOnCode( + PreCode, &Errors, "file.cpp", std::nullopt, ClangTidyOptions(), + {{"foo.h", + R"(#pragma once + #define DECLARE(X) void X() + )"}})); + + PreCode = R"( +#include "foo.h" + +DECLARE { + int a; +} +)"; + + EXPECT_EQ(PreCode, + runCheckOnCode( + PreCode, &Errors, "file.cpp", std::nullopt, ClangTidyOptions(), + {{"foo.h", + R"(#pragma once + #define DECLARE void myfunc() + )"}})); +} + +} // namespace +} // namespace test +} // namespace tidy +} // namespace clang