//===--- Record.cpp - Record compiler events ------------------------------===// // // 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 "clang-include-cleaner/Record.h" #include "clang-include-cleaner/Types.h" #include "clang/AST/ASTConsumer.h" #include "clang/AST/ASTContext.h" #include "clang/AST/DeclGroup.h" #include "clang/Basic/FileEntry.h" #include "clang/Basic/FileManager.h" #include "clang/Basic/LLVM.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/SourceManager.h" #include "clang/Basic/Specifiers.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Lex/DirectoryLookup.h" #include "clang/Lex/MacroInfo.h" #include "clang/Lex/PPCallbacks.h" #include "clang/Lex/Preprocessor.h" #include "clang/Tooling/Inclusions/HeaderAnalysis.h" #include "clang/Tooling/Inclusions/StandardLibrary.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallSet.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/iterator_range.h" #include "llvm/Support/Allocator.h" #include "llvm/Support/Error.h" #include "llvm/Support/FileSystem/UniqueID.h" #include "llvm/Support/StringSaver.h" #include #include #include #include #include #include #include namespace clang::include_cleaner { namespace { class PPRecorder : public PPCallbacks { public: PPRecorder(RecordedPP &Recorded, const Preprocessor &PP) : Recorded(Recorded), PP(PP), SM(PP.getSourceManager()) { for (const auto &Dir : PP.getHeaderSearchInfo().search_dir_range()) if (Dir.getLookupType() == DirectoryLookup::LT_NormalDir) Recorded.Includes.addSearchDirectory(Dir.getDirRef()->getName()); } void FileChanged(SourceLocation Loc, FileChangeReason Reason, SrcMgr::CharacteristicKind FileType, FileID PrevFID) override { Active = SM.isWrittenInMainFile(Loc); } void InclusionDirective(SourceLocation Hash, const Token &IncludeTok, StringRef SpelledFilename, bool IsAngled, CharSourceRange FilenameRange, OptionalFileEntryRef File, StringRef SearchPath, StringRef RelativePath, const Module *, SrcMgr::CharacteristicKind) override { if (!Active) return; Include I; I.HashLocation = Hash; I.Resolved = File; I.Line = SM.getSpellingLineNumber(Hash); I.Spelled = SpelledFilename; I.Angled = IsAngled; Recorded.Includes.add(I); } void MacroExpands(const Token &MacroName, const MacroDefinition &MD, SourceRange Range, const MacroArgs *Args) override { if (!Active) return; recordMacroRef(MacroName, *MD.getMacroInfo()); } void MacroDefined(const Token &MacroName, const MacroDirective *MD) override { if (!Active) return; const auto *MI = MD->getMacroInfo(); // The tokens of a macro definition could refer to a macro. // Formally this reference isn't resolved until this macro is expanded, // but we want to treat it as a reference anyway. for (const auto &Tok : MI->tokens()) { auto *II = Tok.getIdentifierInfo(); // Could this token be a reference to a macro? (Not param to this macro). if (!II || !II->hadMacroDefinition() || llvm::is_contained(MI->params(), II)) continue; if (const MacroInfo *MI = PP.getMacroInfo(II)) recordMacroRef(Tok, *MI); } } void MacroUndefined(const Token &MacroName, const MacroDefinition &MD, const MacroDirective *) override { if (!Active) return; if (const auto *MI = MD.getMacroInfo()) recordMacroRef(MacroName, *MI); } void Ifdef(SourceLocation Loc, const Token &MacroNameTok, const MacroDefinition &MD) override { if (!Active) return; if (const auto *MI = MD.getMacroInfo()) recordMacroRef(MacroNameTok, *MI, RefType::Ambiguous); } void Ifndef(SourceLocation Loc, const Token &MacroNameTok, const MacroDefinition &MD) override { if (!Active) return; if (const auto *MI = MD.getMacroInfo()) recordMacroRef(MacroNameTok, *MI, RefType::Ambiguous); } using PPCallbacks::Elifdef; using PPCallbacks::Elifndef; void Elifdef(SourceLocation Loc, const Token &MacroNameTok, const MacroDefinition &MD) override { if (!Active) return; if (const auto *MI = MD.getMacroInfo()) recordMacroRef(MacroNameTok, *MI, RefType::Ambiguous); } void Elifndef(SourceLocation Loc, const Token &MacroNameTok, const MacroDefinition &MD) override { if (!Active) return; if (const auto *MI = MD.getMacroInfo()) recordMacroRef(MacroNameTok, *MI, RefType::Ambiguous); } void Defined(const Token &MacroNameTok, const MacroDefinition &MD, SourceRange Range) override { if (!Active) return; if (const auto *MI = MD.getMacroInfo()) recordMacroRef(MacroNameTok, *MI, RefType::Ambiguous); } private: void recordMacroRef(const Token &Tok, const MacroInfo &MI, RefType RT = RefType::Explicit) { if (MI.isBuiltinMacro()) return; // __FILE__ is not a reference. Recorded.MacroReferences.push_back( SymbolReference{Macro{Tok.getIdentifierInfo(), MI.getDefinitionLoc()}, Tok.getLocation(), RT}); } bool Active = false; RecordedPP &Recorded; const Preprocessor &PP; const SourceManager &SM; }; } // namespace class PragmaIncludes::RecordPragma : public PPCallbacks, public CommentHandler { public: RecordPragma(const CompilerInstance &CI, PragmaIncludes *Out) : RecordPragma(CI.getPreprocessor(), Out) {} RecordPragma(const Preprocessor &P, PragmaIncludes *Out) : SM(P.getSourceManager()), HeaderInfo(P.getHeaderSearchInfo()), Out(Out), Arena(std::make_shared()), UniqueStrings(*Arena) {} void FileChanged(SourceLocation Loc, FileChangeReason Reason, SrcMgr::CharacteristicKind FileType, FileID PrevFID) override { InMainFile = SM.isWrittenInMainFile(Loc); if (Reason == PPCallbacks::ExitFile) { // At file exit time HeaderSearchInfo is valid and can be used to // determine whether the file was a self-contained header or not. if (OptionalFileEntryRef FE = SM.getFileEntryRefForID(PrevFID)) { if (tooling::isSelfContainedHeader(*FE, SM, HeaderInfo)) Out->NonSelfContainedFiles.erase(FE->getUniqueID()); else Out->NonSelfContainedFiles.insert(FE->getUniqueID()); } } } void EndOfMainFile() override { for (auto &It : Out->IWYUExportBy) { llvm::sort(It.getSecond()); It.getSecond().erase( std::unique(It.getSecond().begin(), It.getSecond().end()), It.getSecond().end()); } Out->Arena.emplace_back(std::move(Arena)); } void InclusionDirective(SourceLocation HashLoc, const Token &IncludeTok, llvm::StringRef FileName, bool IsAngled, CharSourceRange /*FilenameRange*/, OptionalFileEntryRef File, llvm::StringRef /*SearchPath*/, llvm::StringRef /*RelativePath*/, const clang::Module * /*Imported*/, SrcMgr::CharacteristicKind FileKind) override { FileID HashFID = SM.getFileID(HashLoc); int HashLine = SM.getLineNumber(HashFID, SM.getFileOffset(HashLoc)); std::optional
IncludedHeader; if (IsAngled) if (auto StandardHeader = tooling::stdlib::Header::named("<" + FileName.str() + ">")) { IncludedHeader = *StandardHeader; } if (!IncludedHeader && File) IncludedHeader = *File; checkForExport(HashFID, HashLine, std::move(IncludedHeader), File); checkForKeep(HashLine, File); } void checkForExport(FileID IncludingFile, int HashLine, std::optional
IncludedHeader, OptionalFileEntryRef IncludedFile) { if (ExportStack.empty()) return; auto &Top = ExportStack.back(); if (Top.SeenAtFile != IncludingFile) return; // Make sure current include is covered by the export pragma. if ((Top.Block && HashLine > Top.SeenAtLine) || Top.SeenAtLine == HashLine) { if (IncludedFile) Out->IWYUExportBy[IncludedFile->getUniqueID()].push_back(Top.Path); if (IncludedHeader && IncludedHeader->kind() == Header::Standard) Out->StdIWYUExportBy[IncludedHeader->standard()].push_back(Top.Path); // main-file #include with export pragma should never be removed. if (Top.SeenAtFile == SM.getMainFileID() && IncludedFile) Out->ShouldKeep.insert(IncludedFile->getUniqueID()); } if (!Top.Block) // Pop immediately for single-line export pragma. ExportStack.pop_back(); } void checkForKeep(int HashLine, OptionalFileEntryRef IncludedFile) { if (!InMainFile || KeepStack.empty()) return; KeepPragma &Top = KeepStack.back(); // Check if the current include is covered by a keep pragma. if (IncludedFile && ((Top.Block && HashLine > Top.SeenAtLine) || Top.SeenAtLine == HashLine)) { Out->ShouldKeep.insert(IncludedFile->getUniqueID()); } if (!Top.Block) KeepStack.pop_back(); // Pop immediately for single-line keep pragma. } bool HandleComment(Preprocessor &PP, SourceRange Range) override { auto &SM = PP.getSourceManager(); auto Pragma = tooling::parseIWYUPragma(SM.getCharacterData(Range.getBegin())); if (!Pragma) return false; auto [CommentFID, CommentOffset] = SM.getDecomposedLoc(Range.getBegin()); int CommentLine = SM.getLineNumber(CommentFID, CommentOffset); if (InMainFile) { if (Pragma->starts_with("keep")) { KeepStack.push_back({CommentLine, false}); } else if (Pragma->starts_with("begin_keep")) { KeepStack.push_back({CommentLine, true}); } else if (Pragma->starts_with("end_keep") && !KeepStack.empty()) { assert(KeepStack.back().Block); KeepStack.pop_back(); } } auto FE = SM.getFileEntryRefForID(CommentFID); if (!FE) { // This can only happen when the buffer was registered virtually into // SourceManager and FileManager has no idea about it. In such a scenario, // that file cannot be discovered by HeaderSearch, therefore no "explicit" // includes for that file. return false; } auto CommentUID = FE->getUniqueID(); if (Pragma->consume_front("private")) { StringRef PublicHeader; if (Pragma->consume_front(", include ")) { // We always insert using the spelling from the pragma. PublicHeader = save(Pragma->starts_with("<") || Pragma->starts_with("\"") ? (*Pragma) : ("\"" + *Pragma + "\"").str()); } Out->IWYUPublic.insert({CommentUID, PublicHeader}); return false; } if (Pragma->consume_front("always_keep")) { Out->ShouldKeep.insert(CommentUID); return false; } auto Filename = FE->getName(); // Record export pragma. if (Pragma->starts_with("export")) { ExportStack.push_back({CommentLine, CommentFID, save(Filename), false}); } else if (Pragma->starts_with("begin_exports")) { ExportStack.push_back({CommentLine, CommentFID, save(Filename), true}); } else if (Pragma->starts_with("end_exports")) { // FIXME: be robust on unmatching cases. We should only pop the stack if // the begin_exports and end_exports is in the same file. if (!ExportStack.empty()) { assert(ExportStack.back().Block); ExportStack.pop_back(); } } return false; } private: StringRef save(llvm::StringRef S) { return UniqueStrings.save(S); } bool InMainFile = false; const SourceManager &SM; const HeaderSearch &HeaderInfo; PragmaIncludes *Out; std::shared_ptr Arena; /// Intern table for strings. Contents are on the arena. llvm::StringSaver UniqueStrings; struct ExportPragma { // The line number where we saw the begin_exports or export pragma. int SeenAtLine = 0; // 1-based line number. // The file where we saw the pragma. FileID SeenAtFile; // Name (per FileEntry::getName()) of the file SeenAtFile. StringRef Path; // true if it is a block begin/end_exports pragma; false if it is a // single-line export pragma. bool Block = false; }; // A stack for tracking all open begin_exports or single-line export. std::vector ExportStack; struct KeepPragma { // The line number where we saw the begin_keep or keep pragma. int SeenAtLine = 0; // 1-based line number. // true if it is a block begin/end_keep pragma; false if it is a // single-line keep pragma. bool Block = false; }; // A stack for tracking all open begin_keep pragmas or single-line keeps. std::vector KeepStack; }; void PragmaIncludes::record(const CompilerInstance &CI) { auto Record = std::make_unique(CI, this); CI.getPreprocessor().addCommentHandler(Record.get()); CI.getPreprocessor().addPPCallbacks(std::move(Record)); } void PragmaIncludes::record(Preprocessor &P) { auto Record = std::make_unique(P, this); P.addCommentHandler(Record.get()); P.addPPCallbacks(std::move(Record)); } llvm::StringRef PragmaIncludes::getPublic(const FileEntry *F) const { auto It = IWYUPublic.find(F->getUniqueID()); if (It == IWYUPublic.end()) return ""; return It->getSecond(); } static llvm::SmallVector toFileEntries(llvm::ArrayRef FileNames, FileManager &FM) { llvm::SmallVector Results; for (auto FName : FileNames) { // FIMXE: log the failing cases? if (auto FE = FM.getOptionalFileRef(FName)) Results.push_back(*FE); } return Results; } llvm::SmallVector PragmaIncludes::getExporters(const FileEntry *File, FileManager &FM) const { auto It = IWYUExportBy.find(File->getUniqueID()); if (It == IWYUExportBy.end()) return {}; return toFileEntries(It->getSecond(), FM); } llvm::SmallVector PragmaIncludes::getExporters(tooling::stdlib::Header StdHeader, FileManager &FM) const { auto It = StdIWYUExportBy.find(StdHeader); if (It == StdIWYUExportBy.end()) return {}; return toFileEntries(It->getSecond(), FM); } bool PragmaIncludes::isSelfContained(const FileEntry *FE) const { return !NonSelfContainedFiles.contains(FE->getUniqueID()); } bool PragmaIncludes::isPrivate(const FileEntry *FE) const { return IWYUPublic.contains(FE->getUniqueID()); } bool PragmaIncludes::shouldKeep(const FileEntry *FE) const { return ShouldKeep.contains(FE->getUniqueID()) || NonSelfContainedFiles.contains(FE->getUniqueID()); } namespace { template bool isImplicitTemplateSpecialization(const Decl *D) { if (const auto *TD = dyn_cast(D)) return TD->getTemplateSpecializationKind() == TSK_ImplicitInstantiation; return false; } } // namespace std::unique_ptr RecordedAST::record() { class Recorder : public ASTConsumer { RecordedAST *Out; public: Recorder(RecordedAST *Out) : Out(Out) {} void Initialize(ASTContext &Ctx) override { Out->Ctx = &Ctx; } bool HandleTopLevelDecl(DeclGroupRef DG) override { const auto &SM = Out->Ctx->getSourceManager(); for (Decl *D : DG) { if (!SM.isWrittenInMainFile(SM.getExpansionLoc(D->getLocation()))) continue; if (isImplicitTemplateSpecialization(D) || isImplicitTemplateSpecialization(D) || isImplicitTemplateSpecialization(D)) continue; // FIXME: Filter out certain Obj-C as well. Out->Roots.push_back(D); } return ASTConsumer::HandleTopLevelDecl(DG); } }; return std::make_unique(this); } std::unique_ptr RecordedPP::record(const Preprocessor &PP) { return std::make_unique(*this, PP); } } // namespace clang::include_cleaner