aboutsummaryrefslogtreecommitdiff
path: root/clang-tools-extra/clang-tidy/misc/IncludeCleanerCheck.cpp
diff options
context:
space:
mode:
authorViktoriia Bakalova <bakalova@google.com>2023-04-19 07:16:51 +0000
committerViktoriia Bakalova <bakalova@google.com>2023-06-02 15:21:20 +0000
commitc28506ba4b6961950849f8fdecd0cf7e503a14f9 (patch)
tree1bb908df2988925a524f64954b6180096f2c1cc8 /clang-tools-extra/clang-tidy/misc/IncludeCleanerCheck.cpp
parentf9753ef1893c3d953a0244e7935d3997499b079a (diff)
downloadllvm-c28506ba4b6961950849f8fdecd0cf7e503a14f9.zip
llvm-c28506ba4b6961950849f8fdecd0cf7e503a14f9.tar.gz
llvm-c28506ba4b6961950849f8fdecd0cf7e503a14f9.tar.bz2
[clang-tidy] Implement an include-cleaner check.
Differential Revision: https://reviews.llvm.org/D148793
Diffstat (limited to 'clang-tools-extra/clang-tidy/misc/IncludeCleanerCheck.cpp')
-rw-r--r--clang-tools-extra/clang-tidy/misc/IncludeCleanerCheck.cpp202
1 files changed, 202 insertions, 0 deletions
diff --git a/clang-tools-extra/clang-tidy/misc/IncludeCleanerCheck.cpp b/clang-tools-extra/clang-tidy/misc/IncludeCleanerCheck.cpp
new file mode 100644
index 0000000..c7aca83
--- /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 <optional>
+#include <string>
+#include <vector>
+
+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<const include_cleaner::Include *> Used;
+ std::vector<MissingIncludeInfo> Missing;
+ llvm::SmallVector<Decl *> MainFileDecls;
+ for (Decl *D : Result.Nodes.getNodeAs<TranslationUnitDecl>("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<include_cleaner::Header> 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<const include_cleaner::Include *> 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