| //===--- 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 |