mirror of
https://github.com/llvm/llvm-project.git
synced 2025-04-16 08:56:35 +00:00
242 lines
10 KiB
C++
242 lines
10 KiB
C++
//===--- 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/IncludeSpeller.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/HeaderSearchOptions.h"
|
|
#include "clang/Lex/Preprocessor.h"
|
|
#include "clang/Tooling/Core/Replacement.h"
|
|
#include "clang/Tooling/Inclusions/HeaderIncludes.h"
|
|
#include "clang/Tooling/Inclusions/StandardLibrary.h"
|
|
#include "llvm/ADT/DenseSet.h"
|
|
#include "llvm/ADT/STLExtras.h"
|
|
#include "llvm/ADT/SmallVector.h"
|
|
#include "llvm/ADT/StringRef.h"
|
|
#include "llvm/ADT/StringSet.h"
|
|
#include "llvm/Support/ErrorHandling.h"
|
|
#include "llvm/Support/Path.h"
|
|
#include "llvm/Support/Regex.h"
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
using namespace clang::ast_matchers;
|
|
|
|
namespace clang::tidy::misc {
|
|
|
|
namespace {
|
|
struct MissingIncludeInfo {
|
|
include_cleaner::SymbolReference SymRef;
|
|
include_cleaner::Header Missing;
|
|
};
|
|
} // namespace
|
|
|
|
IncludeCleanerCheck::IncludeCleanerCheck(StringRef Name,
|
|
ClangTidyContext *Context)
|
|
: ClangTidyCheck(Name, Context),
|
|
IgnoreHeaders(
|
|
utils::options::parseStringList(Options.get("IgnoreHeaders", ""))),
|
|
DeduplicateFindings(Options.get("DeduplicateFindings", true)) {
|
|
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));
|
|
Options.store(Opts, "DeduplicateFindings", DeduplicateFindings);
|
|
}
|
|
|
|
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));
|
|
this->PP = PP;
|
|
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:
|
|
// We don't trim angle brackets around standard library headers
|
|
// deliberately, so that they are only matched as <vector>, otherwise
|
|
// having just `.*/vector` might yield false positives.
|
|
return R.match(H.standard().name());
|
|
case include_cleaner::Header::Verbatim:
|
|
return R.match(H.verbatim().trim("<>\""));
|
|
case include_cleaner::Header::Physical:
|
|
return R.match(H.physical().getFileEntry().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);
|
|
}
|
|
llvm::DenseSet<include_cleaner::Symbol> SeenSymbols;
|
|
OptionalDirectoryEntryRef ResourceDir =
|
|
PP->getHeaderSearchInfo().getModuleMap().getBuiltinDir();
|
|
// FIXME: Find a way to have less code duplication between include-cleaner
|
|
// analysis implementation and the below code.
|
|
walkUsed(MainFileDecls, RecordedPreprocessor.MacroReferences, &RecordedPI,
|
|
*PP,
|
|
[&](const include_cleaner::SymbolReference &Ref,
|
|
llvm::ArrayRef<include_cleaner::Header> Providers) {
|
|
// Process each symbol once to reduce noise in the findings.
|
|
// Tidy checks are used in two different workflows:
|
|
// - Ones that show all the findings for a given file. For such
|
|
// workflows there is not much point in showing all the occurences,
|
|
// as one is enough to indicate the issue.
|
|
// - Ones that show only the findings on changed pieces. For such
|
|
// workflows it's useful to show findings on every reference of a
|
|
// symbol as otherwise tools might give incosistent results
|
|
// depending on the parts of the file being edited. But it should
|
|
// still help surface findings for "new violations" (i.e.
|
|
// dependency did not exist in the code at all before).
|
|
if (DeduplicateFindings && !SeenSymbols.insert(Ref.Target).second)
|
|
return;
|
|
bool Satisfied = false;
|
|
for (const include_cleaner::Header &H : Providers) {
|
|
if (H.kind() == include_cleaner::Header::Physical &&
|
|
(H.physical() == MainFile ||
|
|
H.physical().getDir() == ResourceDir)) {
|
|
Satisfied = true;
|
|
continue;
|
|
}
|
|
|
|
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, Providers.front()});
|
|
});
|
|
|
|
std::vector<const include_cleaner::Include *> Unused;
|
|
for (const include_cleaner::Include &I :
|
|
RecordedPreprocessor.Includes.all()) {
|
|
if (Used.contains(&I) || !I.Resolved || I.Resolved->getDir() == ResourceDir)
|
|
continue;
|
|
if (RecordedPI.shouldKeep(*I.Resolved))
|
|
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().ends_with(PHeader))
|
|
continue;
|
|
}
|
|
auto StdHeader = tooling::stdlib::Header::named(
|
|
I.quote(), PP->getLangOpts().CPlusPlus ? tooling::stdlib::Lang::CXX
|
|
: tooling::stdlib::Lang::C);
|
|
if (StdHeader && shouldIgnore(*StdHeader))
|
|
continue;
|
|
if (shouldIgnore(*I.Resolved))
|
|
continue;
|
|
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")
|
|
<< llvm::sys::path::filename(Inc->Spelled,
|
|
llvm::sys::path::Style::posix)
|
|
<< 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);
|
|
// Deduplicate insertions when running in bulk fix mode.
|
|
llvm::StringSet<> InsertedHeaders{};
|
|
for (const auto &Inc : Missing) {
|
|
std::string Spelling = include_cleaner::spellHeader(
|
|
{Inc.Missing, PP->getHeaderSearchInfo(), 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)) {
|
|
DiagnosticBuilder DB =
|
|
diag(SM->getSpellingLoc(Inc.SymRef.RefLocation),
|
|
"no header providing \"%0\" is directly included")
|
|
<< Inc.SymRef.Target.name();
|
|
if (areDiagsSelfContained() ||
|
|
InsertedHeaders.insert(Replacement->getReplacementText()).second) {
|
|
DB << FixItHint::CreateInsertion(
|
|
SM->getComposedLoc(SM->getMainFileID(), Replacement->getOffset()),
|
|
Replacement->getReplacementText());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace clang::tidy::misc
|