//===--- 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 #include #include #include 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 &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 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 *, bool, 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 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 + 1, [](const Include &Elem) { return !Elem.Name.empty() && Elem.Loc.isValid(); }); if (!IsIncludePathValid) return; for (const Include &I : llvm::make_range(Files.rbegin(), It + 1)) Check.diag(I.Loc, "'%0' included from here", DiagnosticIDs::Note) << I.Name; } bool isFileIgnored(StringRef FileName) const { return llvm::any_of(IgnoredFilesRegexes, [&](const llvm::Regex &It) { return It.match(FileName); }); } private: std::deque Files; std::optional NextToEnter; HeaderIncludeCycleCheck &Check; const SourceManager &SM; std::vector 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(*this, SM, IgnoredFilesList)); } void HeaderIncludeCycleCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) { Options.store(Opts, "IgnoredFilesList", utils::options::serializeStringList(IgnoredFilesList)); } } // namespace clang::tidy::misc