blob: bebd6e390ed53c65e1c819f24b6202914e4df6ed [file] [log] [blame]
//===--- HeaderIncludeCycleCheck.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 "HeaderIncludeCycleCheck.h"
#include "../utils/OptionsUtils.h"
#include "clang/AST/ASTContext.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Lex/PPCallbacks.h"
#include "clang/Lex/Preprocessor.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/Support/Regex.h"
#include <algorithm>
#include <deque>
#include <optional>
#include <string>
using namespace clang::ast_matchers;
namespace clang::tidy::misc {
namespace {
struct Include {
FileID Id;
llvm::StringRef Name;
SourceLocation Loc;
};
class CyclicDependencyCallbacks : public PPCallbacks {
public:
CyclicDependencyCallbacks(HeaderIncludeCycleCheck &Check,
const SourceManager &SM,
const std::vector<StringRef> &IgnoredFilesList)
: Check(Check), SM(SM) {
IgnoredFilesRegexes.reserve(IgnoredFilesList.size());
for (const StringRef &It : IgnoredFilesList) {
if (!It.empty())
IgnoredFilesRegexes.emplace_back(It);
}
}
void FileChanged(SourceLocation Loc, FileChangeReason Reason,
SrcMgr::CharacteristicKind FileType,
FileID PrevFID) override {
if (FileType != clang::SrcMgr::C_User)
return;
if (Reason != EnterFile && Reason != ExitFile)
return;
FileID Id = SM.getFileID(Loc);
if (Id.isInvalid())
return;
if (Reason == ExitFile) {
if ((Files.size() > 1U) && (Files.back().Id == PrevFID) &&
(Files[Files.size() - 2U].Id == Id))
Files.pop_back();
return;
}
if (!Files.empty() && Files.back().Id == Id)
return;
std::optional<llvm::StringRef> FilePath = SM.getNonBuiltinFilenameForID(Id);
llvm::StringRef FileName =
FilePath ? llvm::sys::path::filename(*FilePath) : llvm::StringRef();
if (!NextToEnter)
NextToEnter = Include{Id, FileName, SourceLocation()};
assert(NextToEnter->Name == FileName);
NextToEnter->Id = Id;
Files.emplace_back(*NextToEnter);
NextToEnter.reset();
}
void InclusionDirective(SourceLocation, const Token &, StringRef FilePath,
bool, CharSourceRange Range,
OptionalFileEntryRef File, StringRef, StringRef,
const Module *,
SrcMgr::CharacteristicKind FileType) override {
if (FileType != clang::SrcMgr::C_User)
return;
llvm::StringRef FileName = llvm::sys::path::filename(FilePath);
NextToEnter = {FileID(), FileName, Range.getBegin()};
if (!File)
return;
FileID Id = SM.translateFile(*File);
if (Id.isInvalid())
return;
checkForDoubleInclude(Id, FileName, Range.getBegin());
}
void EndOfMainFile() override {
if (!Files.empty() && Files.back().Id == SM.getMainFileID())
Files.pop_back();
assert(Files.empty());
}
void checkForDoubleInclude(FileID Id, llvm::StringRef FileName,
SourceLocation Loc) {
auto It =
std::find_if(Files.rbegin(), Files.rend(),
[&](const Include &Entry) { return Entry.Id == Id; });
if (It == Files.rend())
return;
const std::optional<StringRef> FilePath = SM.getNonBuiltinFilenameForID(Id);
if (!FilePath || isFileIgnored(*FilePath))
return;
if (It == Files.rbegin()) {
Check.diag(Loc, "direct self-inclusion of header file '%0'") << FileName;
return;
}
Check.diag(Loc, "circular header file dependency detected while including "
"'%0', please check the include path")
<< FileName;
const bool IsIncludePathValid =
std::all_of(Files.rbegin(), It, [](const Include &Elem) {
return !Elem.Name.empty() && Elem.Loc.isValid();
});
if (!IsIncludePathValid)
return;
auto CurrentIt = Files.rbegin();
do {
Check.diag(CurrentIt->Loc, "'%0' included from here", DiagnosticIDs::Note)
<< CurrentIt->Name;
} while (CurrentIt++ != It);
}
bool isFileIgnored(StringRef FileName) const {
return llvm::any_of(IgnoredFilesRegexes, [&](const llvm::Regex &It) {
return It.match(FileName);
});
}
private:
std::deque<Include> Files;
std::optional<Include> NextToEnter;
HeaderIncludeCycleCheck &Check;
const SourceManager &SM;
std::vector<llvm::Regex> IgnoredFilesRegexes;
};
} // namespace
HeaderIncludeCycleCheck::HeaderIncludeCycleCheck(StringRef Name,
ClangTidyContext *Context)
: ClangTidyCheck(Name, Context),
IgnoredFilesList(utils::options::parseStringList(
Options.get("IgnoredFilesList", ""))) {}
void HeaderIncludeCycleCheck::registerPPCallbacks(
const SourceManager &SM, Preprocessor *PP, Preprocessor *ModuleExpanderPP) {
PP->addPPCallbacks(
std::make_unique<CyclicDependencyCallbacks>(*this, SM, IgnoredFilesList));
}
void HeaderIncludeCycleCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
Options.store(Opts, "IgnoredFilesList",
utils::options::serializeStringList(IgnoredFilesList));
}
} // namespace clang::tidy::misc