blob: 064eccd9cd66784597a95b80de88fd73d152b163 [file] [log] [blame]
//===--- 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/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/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.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, 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")
<< 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);
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.SymRef.RefLocation),
"no header providing \"%0\" is directly included")
<< Inc.SymRef.Target.name()
<< FixItHint::CreateInsertion(
SM->getComposedLoc(SM->getMainFileID(),
Replacement->getOffset()),
Replacement->getReplacementText());
}
}
} // namespace clang::tidy::misc